11
12
import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
13
14
+import hasOwnProperty from 'shared/hasOwnProperty';
15
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
16
const {Dispatcher} = ReactDOMSharedInternals;
16
-import {
17
- checkAttributeStringCoercion,
18
- checkPropStringCoercion,
19
-} from 'shared/CheckStringCoercion';
17
+import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
18
19
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
22
-import {isAttributeNameSafe} from '../shared/DOMProperty';
20
import {SVG_NAMESPACE} from '../shared/DOMNamespaces';
21
import {
22
validatePreloadArguments,
25
getValueDescriptorExpectingEnumForWarning,
26
} from '../shared/ReactDOMResourceValidation';
27
31
-import {precacheFiberNode} from './ReactDOMComponentTree';
32
-import {createHTMLElement, setInitialProperties} from './ReactDOMComponent';
28
+import {setInitialProperties} from './ReactDOMComponent';
29
import {
30
+ precacheFiberNode,
31
getResourcesFromRoot,
35
- isMarkedResource,
36
- markNodeAsResource,
32
+ isOwnedInstance,
33
+ markNodeAsHoistable,
34
} from './ReactDOMComponentTree';
35
36
// The resource types we support. currently they match the form for the as argument.
37
// In the future this may need to change, especially when modules / scripts are supported
38
type ResourceType = 'style' | 'font' | 'script';
39
43
-type HoistableTagType = 'link' | 'meta' | 'title' | 'script' | 'style';
40
+type HoistableTagType = 'link' | 'meta' | 'title';
41
type TResource<T: 'stylesheet' | 'style' | 'script' | 'void'> = {
42
type: T,
43
instance: null | Instance,
101
previousDispatcher = null;
102
}
103
104
+export function prepareToCommitHoistables() {
105
+ tagCaches = null;
106
+}
107
+
108
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
109
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
110
// from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
177
178
const preconnectProps = {rel, crossOrigin, href};
179
if (null === ownerDocument.querySelector(key)) {
179
- const preloadInstance = createHTMLElement(
180
- 'link',
181
- preconnectProps,
182
- ownerDocument,
183
- );
180
+ const preloadInstance = ownerDocument.createElement('link');
181
setInitialProperties(preloadInstance, 'link', preconnectProps);
185
- markNodeAsResource(preloadInstance);
182
+ markNodeAsHoistable(preloadInstance);
183
(ownerDocument.head: any).appendChild(preloadInstance);
184
}
185
}
199
} else if (options != null) {
200
if (
201
typeof options === 'object' &&
205
- options.hasOwnProperty('crossOrigin')
202
+ hasOwnProperty.call(options, 'crossOrigin')
203
) {
204
console.error(
205
'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.',
287
preloadPropsMap.set(key, preloadProps);
288
289
if (null === ownerDocument.querySelector(preloadKey)) {
293
- const preloadInstance = createHTMLElement(
294
- 'link',
295
- preloadProps,
296
- ownerDocument,
297
- );
290
+ const preloadInstance = ownerDocument.createElement('link');
291
setInitialProperties(preloadInstance, 'link', preloadProps);
299
- markNodeAsResource(preloadInstance);
292
+ markNodeAsHoistable(preloadInstance);
293
(ownerDocument.head: any).appendChild(preloadInstance);
294
}
295
}
364
preloadPropsMap.set(key, preloadProps);
365
366
if (null === preloadDocument.querySelector(preloadKey)) {
374
- const preloadInstance = createHTMLElement(
375
- 'link',
376
- preloadProps,
377
- preloadDocument,
378
- );
367
+ const preloadInstance = preloadDocument.createElement('link');
368
setInitialProperties(preloadInstance, 'link', preloadProps);
380
- markNodeAsResource(preloadInstance);
369
+ markNodeAsHoistable(preloadInstance);
370
(preloadDocument.head: any).appendChild(preloadInstance);
371
}
372
}
406
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
407
}
408
const ownerDocument = getDocumentFromRoot(resourceRoot);
420
- instance = createHTMLElement('link', stylesheetProps, ownerDocument);
421
- markNodeAsResource(instance);
409
+ instance = ownerDocument.createElement('link');
410
+ markNodeAsHoistable(instance);
411
setInitialProperties(instance, 'link', stylesheetProps);
412
insertStylesheet(instance, precedence, resourceRoot);
413
}
448
adoptPreloadPropsForScript(scriptProps, preloadProps);
449
}
450
const ownerDocument = getDocumentFromRoot(resourceRoot);
462
- instance = createHTMLElement('script', scriptProps, ownerDocument);
463
- markNodeAsResource(instance);
451
+ instance = ownerDocument.createElement('script');
452
+ markNodeAsHoistable(instance);
453
setInitialProperties(instance, 'link', scriptProps);
465
- (getDocumentFromRoot(resourceRoot).head: any).appendChild(instance);
454
+ (ownerDocument.head: any).appendChild(instance);
455
}
456
457
// Construct a Resource and cache it
683
null ===
684
ownerDocument.querySelector(getPreloadStylesheetSelectorFromKey(key))
685
) {
697
- const preloadInstance = createHTMLElement(
698
- 'link',
699
- preloadProps,
700
- ownerDocument,
701
- );
686
+ const preloadInstance = ownerDocument.createElement('link');
687
setInitialProperties(preloadInstance, 'link', preloadProps);
703
- markNodeAsResource(preloadInstance);
688
+ markNodeAsHoistable(preloadInstance);
689
(ownerDocument.head: any).appendChild(preloadInstance);
690
}
691
}
737
);
738
if (instance) {
739
resource.instance = instance;
755
- markNodeAsResource(instance);
740
+ markNodeAsHoistable(instance);
741
return instance;
742
}
743
744
const styleProps = styleTagPropsFromRawProps(props);
745
const ownerDocument = getDocumentFromRoot(hoistableRoot);
761
- instance = createHTMLElement('style', styleProps, ownerDocument);
746
+ instance = ownerDocument.createElement('style');
747
763
- markNodeAsResource(instance);
748
+ markNodeAsHoistable(instance);
749
setInitialProperties(instance, 'style', styleProps);
750
insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
751
resource.instance = instance;
765
);
766
if (instance) {
767
resource.instance = instance;
783
- markNodeAsResource(instance);
768
+ markNodeAsHoistable(instance);
769
return instance;
770
}
771
777
778
// Construct and insert a new instance
779
const ownerDocument = getDocumentFromRoot(hoistableRoot);
795
- instance = createHTMLElement('link', stylesheetProps, ownerDocument);
796
- markNodeAsResource(instance);
780
+ instance = ownerDocument.createElement('link');
781
+ markNodeAsHoistable(instance);
782
const linkInstance: HTMLLinkElement = (instance: any);
783
(linkInstance: any)._p = new Promise((resolve, reject) => {
784
linkInstance.onload = resolve;
806
);
807
if (instance) {
808
resource.instance = instance;
824
- markNodeAsResource(instance);
809
+ markNodeAsHoistable(instance);
810
return instance;
811
}
812
819
820
// Construct and insert a new instance
821
const ownerDocument = getDocumentFromRoot(hoistableRoot);
837
- instance = createHTMLElement('script', scriptProps, ownerDocument);
838
- markNodeAsResource(instance);
822
+ instance = ownerDocument.createElement('script');
823
+ markNodeAsHoistable(instance);
824
setInitialProperties(instance, 'link', scriptProps);
840
- (getDocumentFromRoot(hoistableRoot).head: any).appendChild(instance);
825
+ (ownerDocument.head: any).appendChild(instance);
826
resource.instance = instance;
827
828
return instance;
904
// Hoistable Element Reconciliation
905
// --------------------------------------
906
907
+type KeyedTagCache = Map<string, Array<Element>>;
908
+type DocumentTagCaches = Map<Document, KeyedTagCache>;
909
+let tagCaches: null | DocumentTagCaches = null;
910
+
911
export function hydrateHoistable(
912
hoistableRoot: HoistableRoot,
913
type: HoistableTagType,
915
internalInstanceHandle: Object,
916
): Instance {
917
const ownerDocument = getDocumentFromRoot(hoistableRoot);
929
- const nodes = ownerDocument.getElementsByTagName(type);
918
931
- const children = props.children;
932
- let child, childString;
933
- if (Array.isArray(children)) {
934
- child = children.length === 1 ? children[0] : null;
935
- } else {
936
- child = children;
937
- }
938
- if (
939
- typeof child !== 'function' &&
940
- typeof child !== 'symbol' &&
941
- child !== null &&
942
- child !== undefined
943
- ) {
944
- if (__DEV__) {
945
- checkPropStringCoercion(child, 'children');
946
- }
947
- childString = '' + (child: any);
948
- } else {
949
- childString = '';
950
- }
951
- nodeLoop: for (let i = 0; i < nodes.length; i++) {
952
- const node = nodes[i];
953
- if (
954
- isMarkedResource(node) ||
955
- node.namespaceURI === SVG_NAMESPACE ||
956
- node.textContent !== childString
957
- ) {
958
- continue;
959
- }
960
- let checkedAttributes = 0;
961
- for (const propName in props) {
962
- const propValue = props[propName];
963
- if (!props.hasOwnProperty(propName)) {
964
- continue;
919
+ let instance: ?Instance = null;
920
+ getInstance: switch (type) {
921
+ case 'title': {
922
+ instance = ownerDocument.getElementsByTagName('title')[0];
923
+ if (
924
+ !instance ||
925
+ isOwnedInstance(instance) ||
926
+ instance.namespaceURI === SVG_NAMESPACE ||
927
+ instance.hasAttribute('itemprop')
928
+ ) {
929
+ instance = ownerDocument.createElement(type);
930
+ (ownerDocument.head: any).insertBefore(
931
+ instance,
932
+ ownerDocument.querySelector('head > title'),
933
+ );
934
}
966
- switch (propName) {
967
- // Reserved props will never have an attribute partner
968
- case 'children':
969
- case 'defaultValue':
970
- case 'dangerouslySetInnerHTML':
971
- case 'defaultChecked':
972
- case 'innerHTML':
973
- case 'suppressContentEditableWarning':
974
- case 'suppressHydrationWarning':
975
- case 'style':
976
- // we advance to the next prop
977
- continue;
978
-
979
- // Name remapped props used by hoistable tag types
980
- case 'className': {
981
- if (__DEV__) {
982
- checkAttributeStringCoercion(propValue, propName);
935
+ setInitialProperties(instance, type, props);
936
+ precacheFiberNode(internalInstanceHandle, instance);
937
+ markNodeAsHoistable(instance);
938
+ return instance;
939
+ }
940
+ case 'link': {
941
+ const cache = getHydratableHoistableCache('link', 'href', ownerDocument);
942
+ const key = type + (props.href || '');
943
+ const maybeNodes = cache.get(key);
944
+ if (maybeNodes) {
945
+ const nodes = maybeNodes;
946
+ for (let i = 0; i < nodes.length; i++) {
947
+ const node = nodes[i];
948
+ if (
949
+ node.getAttribute('href') !==
950
+ (props.href == null ? null : props.href) ||
951
+ node.getAttribute('rel') !==
952
+ (props.rel == null ? null : props.rel) ||
953
+ node.getAttribute('title') !==
954
+ (props.title == null ? null : props.title) ||
955
+ node.getAttribute('crossorigin') !==
956
+ (props.crossOrigin == null ? null : props.crossOrigin)
957
+ ) {
958
+ // mismatch, try the next node;
959
+ continue;
960
}
984
- if (node.getAttribute('class') !== '' + propValue) continue nodeLoop;
985
- break;
961
+ instance = node;
962
+ nodes.splice(i, 1);
963
+ break getInstance;
964
}
987
- case 'httpEquiv': {
965
+ }
966
+ instance = ownerDocument.createElement(type);
967
+ setInitialProperties(instance, type, props);
968
+ (ownerDocument.head: any).appendChild(instance);
969
+ break;
970
+ }
971
+ case 'meta': {
972
+ const cache = getHydratableHoistableCache(
973
+ 'meta',
974
+ 'content',
975
+ ownerDocument,
976
+ );
977
+ const key = type + (props.content || '');
978
+ const maybeNodes = cache.get(key);
979
+ if (maybeNodes) {
980
+ const nodes = maybeNodes;
981
+ for (let i = 0; i < nodes.length; i++) {
982
+ const node = nodes[i];
983
+
984
+ // We coerce content to string because it is the most likely one to
985
+ // use a `toString` capable value. For the rest we just do identity match
986
+ // passing non-strings here is not really valid anyway.
987
if (__DEV__) {
989
- checkAttributeStringCoercion(propValue, propName);
988
+ checkAttributeStringCoercion(props.content, 'content');
989
}
991
- if (node.getAttribute('http-equiv') !== '' + propValue)
992
- continue nodeLoop;
993
- break;
994
- }
995
-
996
- // Booleanish props used by hoistable tag types
997
- case 'contentEditable':
998
- case 'draggable':
999
- case 'spellCheck': {
1000
- if (__DEV__) {
1001
- checkAttributeStringCoercion(propValue, propName);
990
+ if (
991
+ node.getAttribute('content') !==
992
+ (props.content == null ? null : '' + props.content) ||
993
+ node.getAttribute('name') !==
994
+ (props.name == null ? null : props.name) ||
995
+ node.getAttribute('property') !==
996
+ (props.property == null ? null : props.property) ||
997
+ node.getAttribute('http-equiv') !==
998
+ (props.httpEquiv == null ? null : props.httpEquiv) ||
999
+ node.getAttribute('charset') !==
1000
+ (props.charSet == null ? null : props.charSet)
1001
+ ) {
1002
+ // mismatch, try the next node;
1003
+ continue;
1004
}
1003
- if (node.getAttribute(propName) !== '' + propValue) continue nodeLoop;
1004
- break;
1005
+ instance = node;
1006
+ nodes.splice(i, 1);
1007
+ break getInstance;
1008
}
1006
-
1007
- // Boolean props used by hoistable tag types
1008
- case 'async':
1009
- case 'defer':
1010
- case 'disabled':
1011
- case 'hidden':
1012
- case 'noModule':
1013
- case 'scoped':
1014
- case 'itemScope':
1015
- if (propValue !== node.hasAttribute(propName)) continue nodeLoop;
1016
- break;
1017
-
1018
- // The following properties are left out because they do not apply to
1019
- // the current set of hoistable types. They may have special handling
1020
- // requirements if they end up applying to a hoistable type in the future
1021
- // case 'acceptCharset':
1022
- // case 'value':
1023
- // case 'allowFullScreen':
1024
- // case 'autoFocus':
1025
- // case 'autoPlay':
1026
- // case 'controls':
1027
- // case 'default':
1028
- // case 'disablePictureInPicture':
1029
- // case 'disableRemotePlayback':
1030
- // case 'formNoValidate':
1031
- // case 'loop':
1032
- // case 'noValidate':
1033
- // case 'open':
1034
- // case 'playsInline':
1035
- // case 'readOnly':
1036
- // case 'required':
1037
- // case 'reversed':
1038
- // case 'seamless':
1039
- // case 'multiple':
1040
- // case 'selected':
1041
- // case 'capture':
1042
- // case 'download':
1043
- // case 'cols':
1044
- // case 'rows':
1045
- // case 'size':
1046
- // case 'span':
1047
- // case 'rowSpan':
1048
- // case 'start':
1049
-
1050
- default:
1051
- if (isAttributeNameSafe(propName)) {
1052
- const attributeName = propName;
1053
- if (propValue == null && node.hasAttribute(attributeName))
1054
- continue nodeLoop;
1055
- if (__DEV__) {
1056
- checkAttributeStringCoercion(propValue, attributeName);
1057
- }
1058
- if (node.getAttribute(attributeName) !== '' + (propValue: any))
1059
- continue nodeLoop;
1060
- }
1009
}
1062
- checkedAttributes++;
1010
+ instance = ownerDocument.createElement(type);
1011
+ setInitialProperties(instance, type, props);
1012
+ (ownerDocument.head: any).appendChild(instance);
1013
+ break;
1014
}
1015
+ default:
1016
+ throw new Error(
1017
+ `getNodesForType encountered a type it did not expect: "${type}". This is a bug in React.`,
1018
+ );
1019
+ }
1020
1065
- if (node.attributes.length !== checkedAttributes) {
1066
- // We didn't match ever attribute so we abandon this node
1067
- continue nodeLoop;
1021
+ // This node is a match
1022
+ precacheFiberNode(internalInstanceHandle, instance);
1023
+ markNodeAsHoistable(instance);
1024
+ return instance;
1025
+}
1026
+
1027
+function getHydratableHoistableCache(
1028
+ type: HoistableTagType,
1029
+ keyAttribute: string,
1030
+ ownerDocument: Document,
1031
+): KeyedTagCache {
1032
+ let cache: KeyedTagCache;
1033
+ let caches: DocumentTagCaches;
1034
+ if (tagCaches === null) {
1035
+ cache = new Map();
1036
+ caches = tagCaches = new Map();
1037
+ caches.set(ownerDocument, cache);
1038
+ } else {
1039
+ caches = tagCaches;
1040
+ const maybeCache = caches.get(ownerDocument);
1041
+ if (!maybeCache) {
1042
+ cache = new Map();
1043
+ caches.set(ownerDocument, cache);
1044
+ } else {
1045
+ cache = maybeCache;
1046
}
1047
+ }
1048
1070
- // We found a matching instance. We can return early after marking it
1071
- markNodeAsResource(node);
1072
- return node;
1049
+ if (cache.has(type)) {
1050
+ // We use type as a special key that signals that this cache has been seeded for this type
1051
+ return cache;
1052
}
1053
1075
- // There is no matching instance to hydrate, we create it now
1076
- const instance = createHTMLElement(type, props, ownerDocument);
1077
- setInitialProperties(instance, type, props);
1078
- precacheFiberNode(internalInstanceHandle, instance);
1079
- markNodeAsResource(instance);
1054
+ // Mark this cache as seeded for this type
1055
+ cache.set(type, (null: any));
1056
1081
- (ownerDocument.head: any).insertBefore(
1082
- instance,
1083
- type === 'title' ? ownerDocument.querySelector('head > title') : null,
1084
- );
1085
- return instance;
1057
+ const nodes = ownerDocument.getElementsByTagName(type);
1058
+ for (let i = 0; i < nodes.length; i++) {
1059
+ const node = nodes[i];
1060
+ if (
1061
+ !isOwnedInstance(node) &&
1062
+ (type !== 'link' || node.getAttribute('rel') !== 'stylesheet') &&
1063
+ node.namespaceURI !== SVG_NAMESPACE
1064
+ ) {
1065
+ const nodeKey = node.getAttribute(keyAttribute) || '';
1066
+ const key = type + nodeKey;
1067
+ const existing = cache.get(key);
1068
+ if (existing) {
1069
+ existing.push(node);
1070
+ } else {
1071
+ cache.set(key, [node]);
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ return cache;
1077
}
1078
1079
export function mountHoistable(