1.virtual dom模型
Virtual DOM之于react,就好比一个虚拟空间,react的所有工作几乎都是基于Virtual DOM完成的。其中,Virtual DOM模型负责Virtual DOM底层架构的构建工作,它拥有一整套的Virtual DOM标签,并负责虚拟节点及其属性的构建,更新,删除等工作。那么,
1.Virtual DOM模型到底是如何构建虚拟节点?
2.如何更新节点属性?
构建一套简易 Virtual DOM 模型并不复杂,它只需要具备一个 DOM 标签所需的基本 元素即可
:1.标签名 2.节点属性 ,包括样式,属性,事件等 3.子节点 4. 表识 id
示例代码如下:
{
// 标签名
tagName: 'div',
// 属性
properties: {
style: {}
},
// 子节点
children: [],
// 唯一标识
key: 1,
}
Virtual DOM 中的节点称为 ReactNode,它分为3种类型 ReactElement、ReactFragment 和ReactText.其中,ReactElement 又分为 ReactComponentElement 和 ReactDOMElement
1.创建 React 元素
通过jsx创建的虚拟元素最终会编译成调用React 的 createElement 方法。那么,createElement 方法到底做了什呢?我们来解读相关源码(源码路径: /v15.0.0/src/isomorphic/classic/element/ReactElement.js):
Virtual DOM 模型通过createElement创建虚拟元素。
// createElement 只是做了简单的参数修正,返回一个 ReactElement 实例对象,
// 也就是虚拟元素的实例
ReactElement.createElement = function (type, config, children) {
// 初始化参数
var propName;
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;
// 如果存在 config,则提取里面的内容
if (config != null) {
ref = config.ref === undefined ? null : config.ref;
key = config.key === undefined ? null : '' + config.key;
self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source;
// 复制 config 里的内容到 props(如 id 和 className 等)
for (propName in config) {
if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}
// 处理 children,全部挂载到 props 的 children 属性上。如果只有一个参数,直接赋值给 children,
// 否则做合并处理
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength); for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 如果某个 prop 为空且存在默认的 prop,则将默认 prop 赋给当前的 prop
if (type && type.defaultProps) {
var defaultProps = type.defaultProps; for (propName in defaultProps) {
if (typeof props[propName] === 'undefined') {
props[propName] = defaultProps[propName];
}
}
}
// 返回一个 ReactElement 实例对象
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
};
2.初始化组件入口
当使用react创建组件时,首先会调用instantiateReactComponent,这是初始化组件的入口函数,它通过判断node类型来区分不同组件的入口。
1.当node为空时,说明node不存在,则初始化空组ReactEmptyComponent.create(instantiateReactComponent)
2.当node类型为对象时,即是dom标签组件或自定义组件,那么如果element类型为字符串时,则初始化dom标签组件ReactNativeComponent.createInternalComponent (element),否则初始化自定义组件ReactCompositeComponentWrapper()。
3.当node类型为字符串/数字时,则初始化文本组件ReactNativeComponent.create InstanceForText(node)
4.如果是其他情况,则不做处理。
instantiateReactComponent方 法 的 源 码 如 下 ( 源 码 路 径 : /v15.0.0/src/renderers/shared/
reconciler/instantiateReactComponent.js):
// 初始化组件入口 var instance;
// 空组件(ReactEmptyComponent)
function instantiateReactComponent(node, parentCompositeType) {
var instance;
if (node === null || node === false) {
instance = ReactEmptyComponent.create(instantiateReactComponent);
}
if (typeof node === 'object') {
var element = node;
if (typeof element.type === 'string') {
// DOM标签(ReactDOMComponent)
instance = ReactNativeComponent.createInternalComponent(element);
} else if (isInternalComponentType(element.type)) {
// 不是字符串表示的自定义组件暂无法使用,此处将不做组件初始化操作
instance = new element.type(element);
} else {
// 自定义组件(ReactCompositeComponent)
instance = new ReactCompositeComponentWrapper();
}
} else if (typeof node === 'string' || typeof node === 'number') {
// 字符串或数字(ReactTextComponent)
instance = ReactNativeComponent.createInstanceForText(node);
} else {
// 不做处理
}
// 设置实例
instance.construct(node);
// 初始化参数
instance._mountIndex = 0; instance._mountImage = null;
return instance;
}
3.文本组件
当 node 类型为文本节点时是不算 Virtual DOM 元素的,但 React 为了保持渲染的一致性,将其封装为文本组件 ReactDOMTextComponent。
在执行 mountComponent 方法时,ReactDOMTextComponent 通过 transaction.useCreateElement 判断该文本是否是通过 createElement 方法创建的节点,如果是,则为该节点创建相应的标签和标 识 domID,这样每个文本节点也能与其他 React 节点一样拥有自己的唯一标识,同时也拥有了 Virtual DOM diff 的权利。但如果不是通过 createElement 创建的文本,React 将不再为其创建 <span> 和 domID 标识,而是直接返回文本内容。
在执行 receiveComponent 方法时,可以通过 DOMChildrenOperations.replaceDelimitedText
(commentNodes[0], commentNodes[1], nextStringText) 来更新文本内容。
ReactDOMTextComponent 的源码(源码路径:/v15.0.0/src/renderers/dom/shared/ReactDOM-
TextComponent.js)如下:
// 创建文本组件,这是 ReactText,并不是 ReactElement
var ReactDOMTextComponent = function (text) {
// 保存当前的字符串
this._currentElement = text;
this._stringText = '' + text;
// ReactDOMComponentTree 需要使用的参数
this._nativeNode = null;
this._nativeParent = null;
// 属性
this._domID = null;
this._mountIndex = 0;
this._closingComment = null;
this._commentNodes = null;
};
Object.assign(ReactDOMTextComponent.prototype, {
mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {
var domID = nativeContainerInfo._idCounter++;
var openingValue = ' react-text: ' + domID + ' '; var closingValue = ' /react-text ';
this._domID = domID;
this._nativeParent = nativeParent;
// 如果使用 createElement 创建文本标签,则该文本会带上标签和 domID
if (transaction.useCreateElement) {
var ownerDocument = nativeContainerInfo._ownerDocument;
var openingComment = ownerDocument.createComment(openingValue);
var closingComment = ownerDocument.createComment(closingValue);
var lazyTree = DOMLazyTree(ownerDocument.createDocumentFragment()); // 开始标签
DOMLazyTree.queueChild(lazyTree, DOMLazyTree(openingComment));
// 如果是文本类型,则创建文本节点
if (this._stringText) {
DOMLazyTree.queueChild(lazyTree, DOMLazyTree(ownerDocument.createTextNode(this._stringText)));
}
// 结束标签
DOMLazyTree.queueChild(lazyTree, DOMLazyTree(closingComment)); ReactDOMComponentTree.precacheNode(this, openingComment);
this._closingComment = closingComment;
return lazyTree;
} else {
var escapedText = escapeTextContentForBrowser(this._stringText); // 静态页面下直接返回文本
if (transaction.renderToStaticMarkup) {
return escapedText;
}
// 如果不是通过 createElement 创建的文本,则将标签和属性注释掉,直接返回文本内容
return (
'<!--' + openingValue + '-->' + escapedText +
'<!--' + closingValue + '-->');
}
},
// 更新文本内容
receiveComponent: function (nextComponent, transaction) {
if (nextText !== this._currentElement) {
this._currentElement = nextText;
var nextStringText = '' + nextText