一、创建DOM组件
React中Virtual DOM几乎涵盖了所有的原生DOM。React大部分工作都是在Virtual DOM完成的。 ReactDOMComponent针对Virturl DOM主要进行了一下处理:
- 属性的操作,事件的处理
- 子节点的更新
二、如何更新属性
当执行mountComponent时,ReactDOMComponent首先会生成标记和标签。通过createOpenTagMarkupAndPutListeners(transaction) 来处理DOM节点的属性和事件。
- 如果存在事件,则对当前的节点添加事件代理,调用enqueuePutListener(this, propKey, propValue, transaction)
- 如果存在样式,首先对样式合并Object.assign({}, prop.style),然后再调用CSSPropertyOperations.createMarkupForStyles(propValue, this) 创建样式。
- 通过DOMPropertyOperations.createMarkupForProperty(propKey, propValue) 创建属性
- 通过DOMPropertyOperations.crateMarkupForId(this._domID) 创建唯一标识。
源码如下:
//ReactDOMComponent.js
//_createOpenTagMarkupAndPutListeners处理DOM节点的属性和事件
_createOpenTagMarkupAndPutListeners: function(transaction, props) {
//声明ret变量,保存一个标签
var ret = '<' + this._currentElement.type;
//循环拼凑出属性
for (var propKey in props) {
//判断属性是否是props的实例属性,如果不是则跳出
if (!props.hasOwnProperty(propKey)) {
continue;
}
var propValue = props[propKey];
if (propValue == null) {
continue;
}
if (registrationNameModules.hasOwnProperty(propKey)) {
if (propValue) {
//通过enqueuePutListener添加事件代理
enqueuePutListener(this, propKey, propValue, transaction);
}
} else {
if (propKey === STYLE) {
//如果是样式属性的话则合并样式
if (propValue) {
propValue = this._previousStyleCopy = Object.assign({}, props.style);
}
//这里调用CSSPropertyOperations.createMarkupForStyles
propValue = CSSPropertyOperations.createMarkupForStyles(propValue, this);
}
//这里创建属性,
var markup = null;
if (this._tag != null && isCustomComponent(this._tag, props)) {
if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
}
} else {
markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
}
if (markup) {
ret += ' ' + markup;
}
}
}
// For static pages, no need to put React ID and checksum. Saves lots of
// bytes.
//对于静态页,不需要设置react-id
if (transaction.renderToStaticMarkup) {
return ret;
}
//这里设置唯一标识
if (!this._nativeParent) {
ret += ' ' + DOMPropertyOperations.createMarkupForRoot();
}
ret += ' ' + DOMPropertyOperations.createMarkupForID(this._domID);
return ret;
},
复制代码
当执行recevieComponent方法时,ReactDOMComponent会通过this.updateComponent来更新DOM节点属性。
- 先删除不需要的旧属性。
- 如果不需要旧样式,则遍历旧样式集合,并对每个样式进行置空删除
- 如果不需要事件,则将其事件监听的属性去掉,即针对当前的节点取消事件代理:deleteListener(this, propKey)
- 如果旧属性不在新属性集合里时,则需要删除旧属性:DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey)
- 再更新属性
- 如果存在新样式,则将新样式进行合并。
- 如果在旧样式中但是不在新样式中,则清除该样式.
- 如果既在旧样式中也在新样式中,且不相同,则更新该样式styleUpdates[styleName] = nextProp[styleName]
- 如果在新样式中,但不在旧样式中,则直接更新为新样式styleUpdates = nextProp
- 如果存在事件更新,则添加事件监听的属性enqueuePutListener(this, propKey, nextProp, transaction)
- 如果存在新属性,则添加新属性, 或者更新旧的同名属性DOMPropetyOperations.setValueForAttribute(node, propKey, nextProp)
//更新属性
_updateDOMProperties: function(lastProps, nextProps, transaction) {
var propKey;
var styleName;
var styleUpdates;
//循环遍历旧属性,当旧属性不在新属性集合里面的时候则要删除
for (propKey in lastProps) {
//如果新属性实例对象上有这个propKey 或者 propKey在旧属性的原型上,则直接跳过,这样剩下的都是不在
//新属性集合里面的,则都要删除
if (nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null) {
continue;
}
//删除不需要的样式
if (propKey === STYLE) {
var lastStyle = this._previousStyleCopy;
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
this._previousStyleCopy = null;
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (lastProps[propKey]) {
//这里额事件监听属性需要去掉监听,针对当前的节点取消事件代理。
deleteListener(this, propKey);
}
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
//从DOM上删除不必要的属性
DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey);
}
}
//针对新属性,需要加到DOM节点上
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp =
propKey === STYLE ? this._previousStyleCopy :
lastProps != null ? lastProps[propKey] : undefined;
//不在新属性中,或者与旧属性相同,则跳过
if (!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
nextProp == null && lastProp == null) {
continue;
}
//DOM上写入新样式
if (propKey === STYLE) {
if (nextProp) {
if (__DEV__) {
checkAndWarnForMutatedStyle(
this._previousStyleCopy,
this._previousStyle,
this
);
this._previousStyle = nextProp;
}
nextProp = this._previousStyleCopy = Object.assign({}, nextProp);
} else {
this._previousStyleCopy = null;
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
//在旧样式中且不在新样式中,且不相同,则更新该样式
for (styleName in lastProp) {
if (lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
//不存在旧样式,则直接写入新样式
// Relies on `updateStylesByID` not mutating `styleUpdates`.
styleUpdates = nextProp;
}
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
//添加事件监听属性
enqueuePutListener(this, propKey, nextProp, transaction);
} else if (lastProp) {
//
deleteListener(this, propKey);
}
//添加新的属性,或者更新旧的同名属性
} else if (isCustomComponent(this._tag, nextProps)) {
if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
DOMPropertyOperations.setValueForAttribute(
getNode(this),
propKey,
nextProp
);
}
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
var node = getNode(this);
// If we're updating to null or undefined, we should remove the property
// from the DOM node instead of inadvertently setting to a string. This
// brings us in line with the same behavior we have on initial render.
if (nextProp != null) {
DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
} else {
//如果更新为null或者undefined,则执行删除属性操作
DOMPropertyOperations.deleteValueForProperty(node, propKey);
}
}
}
//如果styleUpdates不为空,则设置新样式
if (styleUpdates) {
CSSPropertyOperations.setValueForStyles(
getNode(this),
styleUpdates,
this
);
}
},
复制代码
三、如何更新子节点
在mountComponent的时候,ReactComponent通过this.createContentMarkup处理DOM节点。 首先,获取节点内容props.dangerourslySetInnerHTML,如果存在子节点,则通过this.mountChildren对子节点进行初始化渲染。
//ReactComponent.js
//_createContentMarkup
_createContentMarkup: function(transaction, props, context) {
var ret = '';
//获取子节点渲染出内容
var innerHTML = props.dangerouslySetInnerHTML;
if (innerHTML != null) {
if (innerHTML.__html != null) {
ret = innerHTML.__html;
}
} else {
var contentToUse =
CONTENT_TYPES[typeof props.children] ? props.children : null;
var childrenToUse = contentToUse != null ? null : props.children;
if (contentToUse != null) {
// TODO: Validate that text is allowed as a child of this node
ret = escapeTextContentForBrowser(contentToUse);
} else if (childrenToUse != null) {
//对子节点进行初始化渲染
var mountImages = this.mountChildren(
childrenToUse,
transaction,
context
);
ret = mountImages.join('');
}
}
//是否需要换行
if (newlineEatingTags[this._tag] && ret.charAt(0) === '\n') {
return '\n' + ret;
} else {
return ret;
}
},
复制代码
当receiveComponent,ReactComponet会通过updateDOMChildren来更新DOM内容和节点。