原生组件
原生组件(ReactNativeComponent)是React框架内部处理HTML元素的组件,它继承自ReactComponent。由于HTML元素并没有内部状态一说,只需要通过维护属性(props)即可。
构造函数
原生组件的初始化函数记录了对应的HTML元素标签信息,以方便在渲染时生成相应的HTML代码。HTML标签一般都是闭合的,当然也有例外,例如
,
等,因此,构造函数包含两个参数,第一个参数表示该元素的标签名称,第二个参数指示该标签是否可以自闭合。
function ReactNativeComponent(tag, omitClose) {
this._tagOpen = '<' + tag + ' ';
this._tagClose = omitClose ? '' : '</' + tag + '>';
this.tagName = tag.toUpperCase();
}
组件挂载
原生组件的挂载过程相对比较简单,挂载时会先显式地调用ReactComponent.Mixin.mountComponent方法,前文提到过,Mixin中的同名方法会被覆盖,因此这里需要显式调用ReactComponent中定义的mountComponent方法以完成ref相关的工作。
随后,根据当前原生组件的属性信息,创建相应的HTML代码段,通过调用_createOpenTagMarkup、_createContentMarkup和_tagClose,依次生成开标签、内容代码段和闭合标签。_tagClose在构造函数阶段就被确定,而另外两个函数调用则依赖组件的属性信息。
属性校验
在生成HTML代码前,原生组件会校验当前的属性信息是否符合要求。校验的内容针对syle属性和子元素。属性中定义的style要么为空,要么应当以JSON对象的形式定义各个style信息,如果遇到包含连字符的style属性,统一转化成驼峰规则。例如z-index,在style对象中的键名为zIndex。
原生组件还有可能包含子节点或者内容信息。但属性中children、content只能出现其中之一,包含content的HTML元素不会有子节点;包含子节点的标签也不能设置content。除此之前,React内部还有一个dangerouslySetInnerHTML,它与children和content也是互斥的,这三个属性至多只允许出现其中之一。
创建开标签
在创建开标签时,关注点就在于如何根据属性生成HTML标签的props代码段。HTML的开标签中,可能会出现的内容有:
- 通用的HTML标签属性
- 某些标签特有的属性
- 事件处理函数
- 自定义数据
通用的HTML标签属性中,还有一个style属性,由于该属性相对复杂一些,React中将它单独提出进行了处理。
创建开标签的代码如下 :
function() {
var props = this.props;
var ret = this._tagOpen;
for (var propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
var propValue = props[propKey];
if (propValue == null) {
continue;
}
if (registrationNames[propKey]) {
putListener(this._rootNodeID, propKey, propValue);
} else {
if (propKey === STYLE) {
if (propValue) {
propValue = props.style = merge(props.style);
}
propValue = CSSPropertyOperations.createMarkupForStyles(propValue);
}
var markup =
DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
if (markup) {
ret += ' ' + markup;
}
}
}
创建内容标签
如果该组件包含属性dangerouslySetInnerHTML,则直接返回该该属性的内部值__html作为返回结果,代码如下所示
var innerHTML = this.props.dangerouslySetInnerHTML;
if (innerHTML != null) {
if (innerHTML.__html != null) {
return innerHTML.__html;
}
}
若不包含dangerouslySetInnerHTML,将优先查看是否存在props.content,如果存在,则将content内容执行escape之后返回。escape函数定义在src/utils/escapeTextForBrowser.js中。
在不是前两种情形的话,那么该组件的内容就是包含了其他子元素,这时则调用mountMultiChild来生成内容片段。关于mountMultiChild的细节会单独抽章节介绍。
属性更新
原生组件的属性有可能被更新,有可能更新的是HTML标签自身的属性,也有可能更新的是props中传递的children。涉及到原生组件自身的属性改变,通过调用_updateDOMProperties来实现。
当属性为style、dangerouslySetInnerHTML、content或者事件监听器时,走对应的处理流程。而一般的属性更新,则是通过ReactDOMIDOperation.updatePropertyByID来实现。为了加快查询速度,React会将getElementById的结果缓存在ReactDOMNodeCache中。相关代码如下:
updatePropertyByID: function(id, name, value) {
var node = ReactDOMNodeCache.getCachedNodeByID(id);
invariant(
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name),
'updatePropertyByID(...): %s',
INVALID_PROPERTY_ERRORS[name]
);
DOMPropertyOperations.setValueForProperty(node, name, value);
}
真正执行属性更新的函数,是定义在src/domUtils/DOMPropertyOperations.js中的setValueForProperty,内部都是通过调用node.setAttribute或者node.removeAttribute接口完成。相关代码如下:
setValueForProperty: function(node, name, value) {
if (DOMProperty.isStandardName[name]) {
var mutationMethod = DOMProperty.getMutationMethod[name];
if (mutationMethod) {
mutationMethod(node, value);
} else if (DOMProperty.mustUseAttribute[name]) {
if (DOMProperty.hasBooleanValue[name] && !value) {
node.removeAttribute(DOMProperty.getAttributeName[name]);
} else {
node.setAttribute(DOMProperty.getAttributeName[name], value);
}
} else {
var propName = DOMProperty.getPropertyName[name];
if (!DOMProperty.hasSideEffects[name] || node[propName] !== value) {
node[propName] = value;
}
}
} else if (DOMProperty.isCustomAttribute(name)) {
node.setAttribute(name, value);
}
}
组件卸载
原生组件执行卸载是,除了显式调用ReactComponent.Mixin.unmountComponent之外,还需要卸载其内部的子组件,并删相关的事件监听器。代码如下
unmountComponent: function() {
ReactComponent.Mixin.unmountComponent.call(this);
this.unmountMultiChild();
ReactEvent.deleteAllListeners(this._rootNodeID);
}