天天看点

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

前言

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

一、HostComponent(第一次渲染)

作用:

(1) 创建 DOM 实例

(2) 插入子节点

(3) 初始化事件监听器

源码:

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

对象

源码:

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) 一开始先确定了命名空间,一般是

html

namespace

SVG

namespace

http://www.w3.org/2000/svg

请参考:

https://developer.mozilla.org/zh-CN/docs/Web/SVG

MathML

namespace

http://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

元素:

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

我们来看下

createElement()

precacheFiberNode()

updateFiberProps()

三、createElement

作用:

创建

DOM

元素

源码:

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()

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

对象的属性

源码:

const randomKey = Math.random()
  //转成 36 进制
  .toString(36)
  //从index=2开始截取
  .slice(2);

const internalInstanceKey = '__reactInternalInstance$' + randomKey;

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

复制

解析:

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

Math.random().toString(36).slice(2)
           

复制

五、updateFiberProps

作用:

DOM

对象上创建指向

props

的属性

源码:

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

作用:

插入子节点

源码:

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

属性)关联,最终会形成如下图的关系:

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

(2)

appendInitialChild()

export function appendInitialChild(
  parentInstance: Instance,
  child: Instance | TextInstance,
): void {
  parentInstance.appendChild(child);
}
           

复制

本质就是调用

appendChild()

这个 API

是对

appendAllChildren()

及其内部

function

的讲解,接下来看下

finalizeInitialChildren()

及其内部

function

,接下来内容会很多

七、finalizeInitialChildren

作用:

(1) 初始化

DOM

对象的事件监听器和内部属性

(2) 返回

autoFocus

属性的布尔值

源码:

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

对象

源码:

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()

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()

//在 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()

//浅拷贝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()

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()

,获取

option

child

ReactDOMOptionGetHostProps()

//获取<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()

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

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

  //删除了 dev 代码
}
           

复制

② 执行

ReactDOMSelectGetHostProps()

,设置

<select>

对象属性

ReactDOMSelectGetHostProps()

//设置<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()

//在 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()

//设置 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()

//判断新属性,比如 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()

//初始化 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()

// 设置 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

是正确的:

//确保样式的 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()

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()

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

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

⑤ 不是上述情况的话,则

setValueForProperty()

,为

DOM

节点设置属性值(这个 function 太长了,暂时跳过)

(9) 最后又是一串

switch...case

,对特殊的

DOM

标签进行最后的处理,了解下就好

九、shouldAutoFocusHostComponent

作用:

可以

foucus

的节点会返回

autoFocus

的值,否则返回

false

源码:

//可以 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