HostComponent
1 )概述
- 在
completeWork
当中,我们需要对HostComponent
的一些操作有哪些?- 首先在一次更新而不是初次渲染的情况下
- 需要去
diffProperties
来计算,需要更新的内容 - 也就是在 vdom 中去进行一个对比来判断这一个节点是否需要真的去更新它
- 以此来最低程度的去更新整个 dom 的一个过程
- 对于不同 dom property,它有一些不同的处理方式
2 )源码
定位到 packages/react-reconciler/src/ReactFiberCompleteWork.js#L581文章来源:https://www.toymoban.com/news/detail-818277.html
找到 case HostComponent
文章来源地址https://www.toymoban.com/news/detail-818277.html
case HostComponent: {
// 跳过
popHostContext(workInProgress);
// 跳过
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// 不是第一次渲染,也就是更新的时候
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress); // 跳过
}
} else {
// 第一次挂载渲染
// 没有 props 说明有问题,提示
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;
}
// 跳过
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) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
currentHostContext,
)
) {
// If changes to the hydrated node needs to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
// 第一次挂载
// 注意,这里,创建 element的过程
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 (
// 主要功能: 对dom节点上面的可能的事件监听,需要初始化整个react的事件体系
// 这个函数返回当前节点 是否需要 autoFocus
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
// 如果上面if最终是 autofocus, 就要在这个 workInProgress 上面去增加 Update 的 SideEffect
// 来告诉后续commit的时候要执行一定的操作
markUpdate(workInProgress);
}
workInProgress.stateNode = instance; // 挂载 instance
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
break;
}
- 进入
createInstance
// packages/react-dom/src/client/ReactDOMHostConfig.js#L167 // 这个方法就是创建节点的过程 export function createInstance( type: string, props: Props, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: Object, ): Instance { let parentNamespace: string; if (__DEV__) { // TODO: take namespace into account when validating. const hostContextDev = ((hostContext: any): HostContextDev); validateDOMNesting(type, null, hostContextDev.ancestorInfo); if ( typeof props.children === 'string' || typeof props.children === 'number' ) { const string = '' + props.children; const ownAncestorInfo = updatedAncestorInfo( hostContextDev.ancestorInfo, type, ); validateDOMNesting(null, string, ownAncestorInfo); } parentNamespace = hostContextDev.namespace; } else { parentNamespace = ((hostContext: any): HostContextProd); } // 主要在这里 const domElement: Instance = createElement( type, props, rootContainerInstance, parentNamespace, ); // 后面会进入这个方法 precacheFiberNode(internalInstanceHandle, domElement); // 同上 updateFiberProps(domElement, props); return domElement; }
- 进入
createElement
// 不展开这个方法里面调用的内容 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, 先要通过 document 的api来创建 const ownerDocument: Document = getOwnerDocumentFromRootContainer( rootContainerElement, ); let domElement: Element; let namespaceURI = parentNamespace; // 在React中有区分,不同节点类型,都会有一个定义,比如html普通节点, svg节点等 if (namespaceURI === HTML_NAMESPACE) { namespaceURI = getIntrinsicNamespace(type); } // 如果是 html 类型节点,做一些特殊处理 if (namespaceURI === HTML_NAMESPACE) { if (__DEV__) { isCustomComponentTag = isCustomComponent(type, props); // Should this check be gated by parent namespace? Not sure we want to // allow <SVG> or <mATH>. warning( isCustomComponentTag || type === type.toLowerCase(), '<%s /> is using incorrect casing. ' + 'Use PascalCase for React components, ' + 'or lowercase for HTML elements.', type, ); } // 对script节点进行特殊处理 if (type === 'script') { // Create the script via .innerHTML so its "parser-inserted" flag is // set to true and it does not execute const div = ownerDocument.createElement('div'); div.innerHTML = '<script><' + '/script>'; // eslint-disable-line // This is guaranteed to yield a script element. const firstChild = ((div.firstChild: any): HTMLScriptElement); domElement = div.removeChild(firstChild); } else if (typeof props.is === 'string') { // $FlowIssue `createElement` should be updated for Web Components domElement = ownerDocument.createElement(type, {is: props.is}); } 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 domElement = ownerDocument.createElement(type); // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` // attribute 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. // See https://github.com/facebook/react/issues/13222 if (type === 'select' && props.multiple) { const node = ((domElement: any): HTMLSelectElement); node.multiple = true; } } } else { domElement = ownerDocument.createElementNS(namespaceURI, type); } if (__DEV__) { if (namespaceURI === HTML_NAMESPACE) { if ( !isCustomComponentTag && Object.prototype.toString.call(domElement) === '[object HTMLUnknownElement]' && !Object.prototype.hasOwnProperty.call(warnedUnknownTags, type) ) { warnedUnknownTags[type] = true; warning( false, 'The tag <%s> is unrecognized in this browser. ' + 'If you meant to render a React component, start its name with ' + 'an uppercase letter.', type, ); } } } return domElement; }
- 进入
precacheFiberNode
const internalInstanceKey = '__reactInternalInstance$' + randomKey; // 就是在node上挂载上述属性 // hostInst, node 对应着 internalInstanceHandle, domElement // 相当于在 对应dom节点上,通过 internalInstanceKey 这个 key // 去创建一个指向 fiber 对象的引用 // 后期,我们想要从dom对象上获取对应的 fiber就可以方便获取 export function precacheFiberNode(hostInst, node) { node[internalInstanceKey] = hostInst; }
- 进入
updateFiberProps
const internalEventHandlersKey = '__reactEventHandlers$' + randomKey; // 两个参数 node 和 props, 对应着 domElement, props // 在 domElement 上用一个key 存储 props // props 会对应到上面 domElement 的 attribute, 所以,有对应关系,方便后期取用 export function updateFiberProps(node, props) { node[internalEventHandlersKey] = props; }
- 进入
- 进入
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; // 这个循环的意义:对当前节点下寻找第一层的dom节点或text节点,不会append 嵌套(继续下一层)的dom节点 // 这样做的原因,因为对每个dom节点都会有这样一个执行 completeWork 的阶段 // 也就是当前层只找自己下一层的dom或text, 一层一层的向下找,不会存在忽略的问题 while (node !== null) { if (node.tag === HostComponent || node.tag === HostText) { // 注意这里,挂载节点 // parent 是刚刚创建的 instance // node.stateNode 是当前节点子节点对应的Fiber对象 // 如果说,它的子节点那一层上面发现了原生dom节点或者文本节点,就把它挂载到节点上面 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) { // 向下遍历 node.child.return = node; node = node.child; continue; } // 这里直接 return if (node === workInProgress) { return; } // 没有兄弟节点,向上遍历 while (node.sibling === null) { if (node.return === null || node.return === workInProgress) { return; } node = node.return; } // 这个本来如此,对sibling进行挂载操作 node.sibling.return = node.return; // 循环找 sibling 节点 node = node.sibling; } };
- 进入
appendInitialChild
// 这个函数执行的就是 dom 节点的 appendChild方法 export function appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, ): void { parentInstance.appendChild(child); }
- 进入
- 进入
finalizeInitialChildren
export function finalizeInitialChildren( domElement: Instance, type: string, props: Props, rootContainerInstance: Container, hostContext: HostContext, ): boolean { // 做了初始化事件监听,对创建的节点根据 props 设置对应的 attribute 的封装 setInitialProperties(domElement, type, props, rootContainerInstance); // 返回该节点是否需要 autoFocus return shouldAutoFocusHostComponent(type, props); }
- 进入
setInitialProperties
export function setInitialProperties( domElement: Element, tag: string, rawProps: Object, rootContainerElement: Element | Document, ): void { const isCustomComponentTag = isCustomComponent(tag, rawProps); if (__DEV__) { validatePropertiesInDevelopment(tag, rawProps); if ( isCustomComponentTag && !didWarnShadyDOM && (domElement: any).shadyRoot ) { warning( false, '%s is using shady DOM. Using shady DOM with React can ' + 'cause things to break subtly.', getCurrentFiberOwnerNameInDevOrNull() || 'A component', ); didWarnShadyDOM = true; } } // TODO: Make sure that we check isMounted before firing any of these events. let props: Object; // 看下面的 switch 大部分都是绑定事件 // 大部分都是绑定事件,这个叫做 trapBubbledEvent ,这个方法就是用来绑定事件的 // react它有一个自己开发的事件体系,它是在dom的事件体系上面去包装了一层 // 比如说他这边传入了这个 TOP_LOAD // 它有一些事件类型,它会区分不同的一些事件类型来进行一个不同的事件绑定的方式,然后会有很多的事件的一个依赖关系 // 可以看到这边有各种各样的不同的事件绑定。比如说 TOP_ERROR, TOP_TOGGLE, TOP_INVALID, TOP_SUBMIT // 对于input标签,option,select,taxtarea 都会调用对应的处理程序,如 ReactDOMInput, ReactDOMTextarea等 // 它们都有一定的封装,因为对于这些,我们认为它是一个交互组件,就是我们要进行一些输入用户操作的一些组件 // 比如说,input它调用的这个方法叫做 ReactDOMInput.initWrapperState 这个方法 switch (tag) { case 'iframe': case 'object': trapBubbledEvent(TOP_LOAD, domElement); props = rawProps; break; case 'video': case 'audio': // Create listener for each media event for (let i = 0; i < mediaEventTypes.length; i++) { trapBubbledEvent(mediaEventTypes[i], domElement); } props = rawProps; break; case 'source': trapBubbledEvent(TOP_ERROR, domElement); props = rawProps; break; case 'img': case 'image': case 'link': trapBubbledEvent(TOP_ERROR, domElement); trapBubbledEvent(TOP_LOAD, domElement); props = rawProps; break; case 'form': trapBubbledEvent(TOP_RESET, domElement); trapBubbledEvent(TOP_SUBMIT, domElement); props = rawProps; break; case 'details': trapBubbledEvent(TOP_TOGGLE, domElement); props = rawProps; break; case 'input': ReactDOMInput.initWrapperState(domElement, rawProps); props = ReactDOMInput.getHostProps(domElement, rawProps); trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); break; case 'option': ReactDOMOption.validateProps(domElement, rawProps); props = ReactDOMOption.getHostProps(domElement, rawProps); break; case 'select': ReactDOMSelect.initWrapperState(domElement, rawProps); props = ReactDOMSelect.getHostProps(domElement, rawProps); trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); break; case 'textarea': ReactDOMTextarea.initWrapperState(domElement, rawProps); props = ReactDOMTextarea.getHostProps(domElement, rawProps); trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); break; default: props = rawProps; } // 验证 props 并给出提醒 assertValidProps(tag, props); // 初始化dom的属性相关逻辑 setInitialDOMProperties( tag, domElement, rootContainerElement, props, isCustomComponentTag, ); 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. inputValueTracking.track((domElement: any)); ReactDOMInput.postMountWrapper(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. inputValueTracking.track((domElement: any)); ReactDOMTextarea.postMountWrapper(domElement, rawProps); break; case 'option': ReactDOMOption.postMountWrapper(domElement, rawProps); break; case 'select': ReactDOMSelect.postMountWrapper(domElement, rawProps); break; default: if (typeof props.onClick === 'function') { // TODO: This cast may not be sound for SVG, MathML or custom elements. trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); } break; } }
- 进入
ReactDOMInput.initWrapperState
// packages/react-dom/src/client/ReactDOMInput.js export function initWrapperState(element: Element, props: Object) { // 跳过 DEV if (__DEV__) { ReactControlledValuePropTypes.checkPropTypes('input', props); if ( props.checked !== undefined && props.defaultChecked !== undefined && !didWarnCheckedDefaultChecked ) { warning( false, '%s contains an input of type %s with both checked and defaultChecked props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the checked prop, or the defaultChecked prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', getCurrentFiberOwnerNameInDevOrNull() || 'A component', props.type, ); didWarnCheckedDefaultChecked = true; } if ( props.value !== undefined && props.defaultValue !== undefined && !didWarnValueDefaultValue ) { warning( false, '%s contains an input of type %s with both value and defaultValue props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the value prop, or the defaultValue prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', getCurrentFiberOwnerNameInDevOrNull() || 'A component', props.type, ); didWarnValueDefaultValue = true; } } const node = ((element: any): InputWithWrapperState); // 关于 defaultValue, 如果不希望使用value来绑定input它的一个值 // 如果绑定了value这个值,我们input的value,只能通过 setState 这个value的值来进行一个变更 // 如果我们没有绑定事件,那么我们在input里面输入是不会有任何变化的 // 那这个时候我们如果要有初始的值,又不希望去绑定 value // 可以通过使用 defaultValue,如果没有 defaultvalue 它就是一个空的 const defaultValue = props.defaultValue == null ? '' : props.defaultValue; // 在input对应的dom节点上面呢去挂载了一个属性,叫做 _wrapperState // 它里面定义了3个属性 node._wrapperState = { initialChecked: props.checked != null ? props.checked : props.defaultChecked, initialValue: getToStringValue( props.value != null ? props.value : defaultValue, ), controlled: isControlled(props), }; }
- 进入
isControlled
// 处理 checkbox 和 radio function isControlled(props) { const usesChecked = props.type === 'checkbox' || props.type === 'radio'; return usesChecked ? props.checked != null : props.value != null; }
- 进入
- 进入
getHostProps
export function getHostProps(element: Element, props: Object) { const node = ((element: any): InputWithWrapperState); const checked = props.checked; // 组合 props const hostProps = Object.assign({}, props, { defaultChecked: undefined, defaultValue: undefined, value: undefined, checked: checked != null ? checked : node._wrapperState.initialChecked, }); return hostProps; }
- 对于
assertValidProps
和setInitialDOMProperties
- 这样两项封装的Dom操作细节处理,不展开
- 前者做提示处理,后者做dom操作
- 进入
- 进入
shouldAutoFocusHostComponent
function shouldAutoFocusHostComponent(type: string, props: Props): boolean { // 对可以 focus 的 dom节点进行 bool 处理,注意,这里没有 break switch (type) { case 'button': case 'input': case 'select': case 'textarea': return !!props.autoFocus; } return false; }
- 进入
- 进入
markUpdate
// 标记 Update 这个 SideEffect function markUpdate(workInProgress: Fiber) { // Tag the fiber with an update effect. This turns a Placement into // a PlacementAndUpdate. workInProgress.effectTag |= Update; }
- 关于 更新时的
updateHostComponent
这块后续单独拿出来说
到了这里,关于React16源码: React中的completeWork对HostComponent处理的源码实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!