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;
if (nextStringText !== this._stringText) {
this._stringText = nextStringText;
var commentNodes = this.getNativeNode();
DOMChildrenOperations.replaceDelimitedText(commentNodes[0], commentNodes[1], nextStringText);
}
}
},
});
4.DOM标签组件
Virtual DOM 标签的处理主要分为以下两个部分:
1.属性的更新,包括更新样式、更新属性、处理事件等;
当执行 mountComponent 方法时,ReactDOMComponent 首先会生成标记和标签,通过 this. createOpenTagMarkupAndPutListeners(transaction) 来处理 DOM 节点的属性和事件
- 如果存在事件,则针对当前的节点添加事件代理,即调用 enqueuePutListener(this, propKey, propValue, transaction)。
- 如果存在样式,首先会对样式进行合并操作Object.assign({}, props.style),然后通过CSSPropertyOperations.createMarkupForStyles(propValue, this) 创建样式。
- 通过DOMPropertyOperations.createMarkupForProperty(propKey, propValue)创建属性。
- 通过DOMPropertyOperations.createMarkupForID(this._domID)创建唯一标识。
- 如果存在事件,则针对当前的节点添加事件代理,即调用 enqueuePutListener(this, propKey, propValue, transaction)。
- 如果存在样式,首先会对样式进行合并操作Object.assign({}, props.style),然后通过CSSPropertyOperations.createMarkupForStyles(propValue, this) 创建样式。
- 通过DOMPropertyOperations.createMarkupForProperty(propKey, propValue)创建属性。
- 通过DOMPropertyOperations.createMarkupForID(this._domID)创建唯一标识。
_createOpenTagMarkupAndPutListeners: function(transaction, props) {
var ret = '<' + this._currentElement.type;
// 拼凑出属性
for (var propKey in props) {
var propValue = props[propKey];
if (registrationNameModules.hasOwnProperty(propKey)) { // 针对当前的节点添加事件代理
if (propValue) {
enqueuePutListener(this, propKey, propValue, transaction);
}
} else {
if (propKey === STYLE) {
if (propValue) {
// 合并样式
propValue = this._previousStyleCopy = Object.assign({}, props.style);
}
propValue = CSSPropertyOperations.createMarkupForStyles(propValue, this);
}
// 创建属性标识
var markup = null;
if (this._tag != null && isCustomComponent(this._tag, props)) {
markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
}
if (markup) {
ret += ' ' + markup;
}
}
}
// 对于静态页面,不需要设置 react-id,这样可以节省大量字节 if (transaction.renderToStaticMarkup) {
return ret;
}
// 设置 react-id
if (!this._nativeParent) {
ret += ' ' + DOMPropertyOperations.createMarkupForRoot();
}
ret += ' ' + DOMPropertyOperations.createMarkupForID(this._domID);
return ret
}
当执行 receiveComponent 方 法 时 , ReactDOMComponent 会 通 过 this.updateComponent (transaction, prevElement, nextElement, context) 来更新 DOM 节点属性。
先是删除不需要的旧属性。如果不需要旧样式,则遍历旧样式集合,并对每个样式进行置空 删除;如果不需要事件,则将其事件监听的属性去掉,即针对当前的节点取消事件代理 deleteListener(this, propKey);如果旧属性不在新属性集合里时,则需要删除旧属性 DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey)。
再是更新新属性。如果存在新样式,则将新样式进行合并 Object.assign({}, nextProp);如 果在旧样式中但不在新样式中,则清除该样式;如果既在旧样式中也在新样式中,且不相同,则 更新该样式 styleUpdates[styleName] = nextProp[styleName];如果在新样式中,但不在旧样式 中,则直接更新为新样式 styleUpdates = nextProp;如果存在事件更新,则添加事件监听的属 性 enqueuePutListener(this, propKey, nextProp, transaction);如果存在新属性,则添加新属性,或者更新旧的同名属性DOMPropertyOperations.setValueForAttribute(node, propKey,
nextProp)。
至此,ReactDOMComponent 完成了 DOM 节点属性更新的操作,相关代码如下
function _updateDOMProperties(lastProps, nextProps, transaction) {
var propKey;
var styleName;
var styleUpdates;
// 当一个旧的属性不在新的属性集合里时,需要删除
for (propKey in lastProps) {
// 如果新属性里有,或者 propKey 是在原型上的则直接跳过,这样剩下的都是不在新属性集合里的,
// 需要删除
if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
continue;
}
// 从 DOM 上删除不需要的样式
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.isStandardName[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) {
nextProp = this._previousStyleCopy = Object.assign({}, nextProp);
}
if (lastProp) {
// 在旧样式中且不在新样式中,清除该样式
for (styleName in lastProp) {
if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
styleUpdates = styleUpdates || {}; styleUpdates[styleName] = '';
}
}
// 既在旧样式中也在新样式中,且不相同,更新该样式
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// 不存在旧样式,直接写入新样式
styleUpdates = nextProp;
}
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
// 添加事件监听的属性
enqueuePutListener(this, propKey, nextProp, transaction);
} else {
deleteListener(this, propKey);
}
// 添加新的属性,或者是更新旧的同名属性
} else if (isCustomComponent(this._tag, nextProps)) {
if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
// setValueForAttribute 更新属性
DOMPropertyOperations.setValueForAttribute(getNode(this), propKey, nextProp);
}
} else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
var node = getNode(this); if (nextProp != null) {
DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
} else {
// 如果更新为 null 或 undefined,则执行删除属性操作
DOMPropertyOperations.deleteValueForProperty(node, propKey);
}
}
// 如果 styleUpdates 不为空,则设置新样式
if (styleUpdates) {
CSSPropertyOperations.setValueForStyles(getNode(this), styleUpdates, this);
}
}
}
2.子节点的更新,包括更新内容、更新子节点,此部分涉及 diff 算法。
当执行mountComponent方法时,ReactDOMComponent会通过this._createContentMarkup (transaction, props, context) 来处理dom节点的内容。
首先,获取节点内容props.dangerouslySetInnerHTML。如果存在子节点,则通过this. mountChildren(childrenToUse, transaction, context) 对子节点进行初始化渲染:
_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) {
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 方法时,ReactDOMComponent 会通过 this._updateDOMChildren
(lastProps, nextProps, transaction, context) 来更新 DOM 内容和子节点。
先是删除不需要的子节点和内容。如果旧节点存在,而新节点不存在,说明当前节点在更新 后被删除,此时执行方法 this.updateChildren(null, transaction, context);如果旧的内容存 在,而新的内容不存在,说明当前内容在更新后被删除,此时执行方法 this.updateText- Content('')。
再是更新子节点和内容。如果新子节点存在,则更新其子节点,此时执行方法 this.update- Children(nextChildren, transaction, context);如果新的内容存在,则更新内容,此时执行方 法 this.updateTextContent('' + nextContent)。
至此,ReactDOMComponent 完成了 DOM 子节点和内容的更新操作,相关代码如下:
_updateDOMChildren: function(lastProps, nextProps, transaction, context) {
// 初始化
var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
var nextContent = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
var lastHtml = lastProps.dangerouslySetInnerHTML && lastProps.dangerouslySetInnerHTML.__html; var nextHtml = nextProps.dangerouslySetInnerHTML && nextProps.dangerouslySetInnerHTML.__html;
var lastChildren = lastContent != null ? null : lastProps.children; var nextChildren = nextContent != null ? null : nextProps.children; var lastHasContentOrHtml = lastContent != null || lastHtml != null; var nextHasContentOrHtml = nextContent != null || nextHtml != null;
if (lastChildren != null && nextChildren == null) {
// 旧节点存在,而新节点不存在,说明当前节点在更新后被删除了
this.updateChildren(null, transaction, context);
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) { // 说明当前内容在更新后被删除了
this.updateTextContent('');
}
// 新节点存在
if (nextContent != null) {
// 更新内容
if (lastContent !== nextContent) {
this.updateTextContent('' + nextContent);
}
} else if (nextHtml != null) {
// 更新属性标识
if (lastHtml !== nextHtml) {
this.updateMarkup('' + nextHtml);
}
} else if (nextChildren != null) {
// 更新子节点
this.updateChildren(nextChildren, transaction, context);
}
}
当卸载组件时,ReactDOMComponent 会进行一系列的操作,如卸载子节点、清除事件监听、 清空标识等:
unmountComponent: function(safely) {
this.unmountChildren(safely);
ReactDOMComponentTree.uncacheNode(this); EventPluginHub.deleteAllListeners(this); ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID); this._rootNodeID = null;
this._domID = null;
this._wrapperState = null;
}
ReactDOMComponent 关系图
5.自定义组件
自定义组件实现了一整套react生命周期和setState机制,因此自定义组件是在生命周期的环境中进行更新属性,内容和子节点的操作。这些更新操作与ReactDOMComponent的操作类似。
2.生命周期的管理艺术
https://juejin.cn/post/6844904199923187725#heading-11
3.解密setState机制
当this.setState() 被调用的时候,React 会重新调用 render 方法来重新渲染 UI。
1.setState异步更新
setState通过一个队列机制实现state更新,当执行setState时,会将需要更新的state合并后放入状态队列,而不会立刻更新this.state,队列机制可以高效的批量更新state。如果不通过setState而直接修改this.state的值,那么该state不会被放入状态队列中,当下次调用setState并对状态队列进行合并时,将会忽略之前直接被修改的state,而造成无法预知的错误。因此,应该使用setState方法来更新state,同时react也正是利用状态队列机制实现了setState的异步更新,避免频繁的重复更新state。
相关代码如下:
// 将新的 state 合并到状态更新队列中
var nextState = this._processPendingState(nextProps, nextContext);
// 根据更新队列和 shouldComponentUpdate 的状态来判断是否需要更新组件
var shouldUpdate = this._pendingForceUpdate ||
!inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext);
2.setState循环调用风险
当调用setState时,实际上执行enqueueSetState方法,并对partialState以及pendingStateQueue更新队列进行合并操作,最终通过enqueueUpdate执行state更新。当调用 setState 时,实际上会执行 enqueueSetState 方法,并对 partialState 以及_pending-ForceUpdate,并调用 receiveComponent 和 updateComponent 方法进行组件更新。
如 果 在 shouldComponentUpdate 或 componentWillUpdate 方 法 中 调 用 setState , 此 时 this._pendingStateQueue != null,则 performUpdateIfNecessary 方法就会调用 updateComponent 方法进行组件更新,但 updateComponent 方法又会调用 shouldComponentUpdate 和 componentWill- Update 方法,因此造成循环调用,使得浏览器内存占满后崩溃
setState源码:
// 更新 state
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
enqueueSetState: function(publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState');
if (!internalInstance) {
return;
}
// 更新队列合并操作
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
},
// 如果存在 _pendingElement、_pendingStateQueue和_pendingForceUpdate,则更新组件
performUpdateIfNecessary: function(transaction) {
if (this._pendingElement != null) {
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
}
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
}
}
3.setState调用栈
setState最终是通过enqueueUpdate执行state更新,那么enqueueUpdate到底是如何更新state的呢?enqueueUpdate到底做了什么?
enqueueUpdate 的代码如下(源码路径:/v15.0.0/src/ renderers/shared/reconciler/ReactUpdates.
js)
function enqueueUpdate(component) {
ensureInjected();
// 如果不处于批量更新模式
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果处于批量更新模式,则将该组件保存在 dirtyComponents 中
dirtyComponents.push(component);
}
如果isBatchingUpdates为true,则对多有队列中的更新执行batchedUpdates方法,否则只把当前组件放入dirtyComponents数组中。
batchingStrategy做了什么?
其实它只是一个简单的对象,定义了一个 isBatchingUpdates的布尔值,以及batchedUpdates方法(源码路径:/v15.0.0/src/renderers/shared/ reconciler/ReactDefaultBatchingStrategy.js):
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
if (alreadyBatchingUpdates) {
callback(a, b, c, d, e);
} else {
// 事务
transaction.perform(callback, null, a, b, c, d, e);
}
},
}
setState 简化调用栈
4.初识事务
定义:提供可靠的恢复机制,保证出错时数据的一致性,并且不同事务之间互相独立。
事务流程图:
事务就是将需要执行的方法使用wrapper封装起来,再通过事务提供的perform方法执行。而在perform之前,先执行所有wrapper中的initlallize方法,执行完perfom之后,在执行所有的close方法。一组 initialize 及 close 方法称为一个 wrapper。
事务支持多个wrapper叠加。
简单的事务案例:
var Transaction = require('./Transaction');
// 我们自己定义的事务
var MyTransaction = function () {
// 。。。
}
Object.assign(MyTransaction.prototype, Transaction.Mixin, {
getTransactionWrappers: function () {
return [{
initialize: function () {
console.log('before method perform');
},
close: function () {
console.log('after method perform');
}
}];
}
});
var transaction = new MyTransaction();
var testMethod = function () {
console.log('test');
}
transaction.perform(testMethod);
// 打印的结果如下:
// before method perform // test
// after method perform
5.解密setState
整个react组件渲染到dom中的过程就处于一个大的事务中。
4.diff算法
react将Virtual DOM树转换成actual DOM 树的最少操作的过程称为调和。diff算法便是调和的具体实现。
diff算法的3个策略:
- 策略1.Web UI 中 DOM节点跨层级的移动操作特别少,可以忽略不计。
- 策略2.拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
- 策略3.对于同一层级的一组子节点,它们可以通过唯一id进行区分。
基于以上策略,react分别对tree diff,component diff以及element diff进行算法优化,事实也证明了这3个前提策略是合理且准确的,它保证了整体界面构建和性能。
1.tree diff
1:基于策略1,react对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层此的节点进行比较。既然dom节点跨层级的移动操作少到可以忽略不计,针对这一现象,react通过updateDepth 对Virtual DOM 树进行层级控制,只会对相同层级的dom节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在时,则该节点及其子节点完全被删除,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个dom树的比较。
2: updateChildren 方法对应的源码如下:
updateChildren: function(nextNestedChildrenElements, transaction, context) {
updateDepth++;
var errorThrown = true;
try {
this._updateChildren(nextNestedChildrenElements, transaction, context);
errorThrown = false;
} finally {
updateDepth--;
if (!updateDepth) {
if (errorThrown) {
clearQueue();
} else {
processQueue();
}
}
}
}
3:如果出现了DOM节点跨层级的移动操作,diff会有怎么样的表现?
如上图如果A 节点(包括其子节点)整个被移动到 D 节点下,由于react只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只会创建和删除。当根节点发现子节点中A消失了,就会直接销毁A,当D发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。
此时,diff 的执行情况:create A → create B → create C → delete A。
由此可以发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节 点的整个树被重新创建。这是一种影响 React 性能的操作,因此官方建议不要进行 DOM 节点跨 层级的操作。
2.component diff
1.如果是同一类型的组件,按照策略继续比较Virtual DOM树即可。
2.如果不是,则将该组件判断为dirty component,从而替换整个组件下的所有子节点。
3.对于同一类型的组件,有可能其Virtual DOM没有任何变化,如果能够确切知道这点,那么就可以节省大量的diff运算时间。因此,react允许用户通过shouldComponentUpdate()来判断该组件是否需要进行diff算法分析。
如上图,当组件D 变为组件 G 时,即使这两个组件结构类似,一旦react判断D和G是不同类型的组件,就不会比较二者的结构,而是直接删除组件D,重新创建组件G及其子节点。虽然当两个组件是不同类型单结构相似时,diff会影响性能,但正如react官方博客所言,不同类型的组件很少存在相似的dom树的情况,因此这种极端因素很难在实际开发过程中造成重大的影响。
3.element diff
1.当节点处于同一层级时,diff提供了3种节点操作,分别为:插入(INSERT_MARKUP),移动(MOVE_EXISTING),删除(REMOVE_NODE)
1.插入。 新的组件类型不在旧集合里,即全新的节点,需要对新节点执行插入操作。
2.移动 。 旧集合中有新组件类型,且element是可更新的类型,generateComponent- Children 已调用receiveComponent,这种情况下prevChild=nextChild,就需要做移动操作,可以复用以前的dom节点。
3.删除。 旧组件类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作,或者组件不再新集合里,也需要执行删除操作。
相关代码如下:
function makeInsertMarkup(markup, afterNode, toIndex) {
return {
type: ReactMultiChildUpdateTypes.INSERT_MARKUP, content: markup,
fromIndex: null,
fromNode: null,
toIndex: toIndex,
afterNode: afterNode,
};
}
function makeMove(child, afterNode, toIndex) {
return {
type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
content: null,
fromIndex: child._mountIndex,
fromNode: ReactReconciler.getNativeNode(child), toIndex: toIndex,
afterNode: afterNode
};
}
function makeRemove(child, node) {
return {
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
content: null,
fromIndex: child._mountIndex,
fromNode: node, toIndex: null,
afterNode: null,
};
}
案例解析:
1.移动操作(新旧集合中存在相同的节点但是位置不同时)
首先,对新集合中的节点进行循环遍历 for (name in nextChildren),通过唯一的 key 判断 新旧集合中是否存在相同的节点 if (prevChild === nextChild),如果存在相同节点,则进行移 动操作,但在移动前需要将当前节点在旧集合中的位置与 lastIndex 进行比较 if (child._mountIndex < lastIndex),否则不执行该操作。lastIndex 一 直在更新,表示访问过的节点在旧集合中最右的位置(即最大的位置)。如果新集合中当前访问 的节点比 lastIndex 大,说明当前访问节点在旧集合中就比上一个节点位置靠后,则该节点不会 影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作。只有当访问的节点比 lastIndex 小时,才需要进行移动操作。
如上图diff差异对比过程:
1.从新集合中取得 B,然后判断旧集合中是否存在相同节点 B,此时发现存在节点 B,接着通过对比节点位置判断是否进行移动操作。B 在旧集合中的位置 B._mountIndex = 1,此时 lastIndex = 0,不满足 child._mountIndex < lastIndex 的条件,因此不对 B 进行移动 操作。更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中 prevChild._ mountIndex 表示B在旧集合中的位置,则lastIndex = 1,并将B的位置更新为新集合中的 位置 prevChild._mountIndex = nextIndex,此时新集合中 B._mountIndex = 0,nextIndex++ 进入下一个节点的判断。
2.从新集合中取得 A,然后判断旧集合中是否存在相同节点 A,此时发现存在节点 A,接着 通过对比节点位置判断是否进行移动操作。A 在旧集合中的位置 A._mountIndex = 0,此 时 lastIndex = 1,满足 child._mountIndex < lastIndex 的条件,因此对 A 进行移动操作 enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其实就是 nextIndex,表 示 A 需要移动到的位置。更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex), 则lastIndex = 1,并将 A 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 A._mountIndex = 1,nextIndex++ 进入下一个节点的判断。
3. 从新集合中取得 D,然后判断旧集合中是否存在相同节点 D,此时发现存在节点 D,接着 通过对比节点位置判断是否进行移动操作。D 在旧集合中的位置 D._mountIndex = 3,此
时 lastIndex = 1,不满足 child._mountIndex < lastIndex 的条件,因此不对 D 进行移 动操作。更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 D 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集 6 合中 D._mountIndex = 2,nextIndex++ 进入下一个节点的判断。
4.从新集合中取得 C,然后判断旧集合中是否存在相同节点 C,此时发现存在节点 C,接着 通过对比节点位置判断是否进行移动操作。C 在旧集合中的位置 C._mountIndex = 2,此
时 lastIndex = 3,满足 child._mountIndex < lastIndex的条件,因此对 C 进行移动操作 enqueueMove(this, child._mountIndex, toIndex)。更新lastIndex = Math.max(prevChild. 7 _mountIndex, lastIndex),则 lastIndex = 3,并将 C 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 A._mountIndex = 3,nextIndex++ 进 入下一个节点的判断。由于 C 已经是最后一个节点,因此 diff 操作到此完成。
2.创建,移动和删除(新集合中有新加入的节点且旧集合存在需要删除的节点)
diff过程:
1.从新集合中取得B,然后判断旧集合中存在是否相同节点 B,可以发现存在节点 B。由于 B 在旧集合中的位置 B._mountIndex = 1,此时 lastIndex = 0,因此不对 B 进行移动操作。 更新lastIndex = 1,并将 B 的位置更新为新集合中的位置 B._mountIndex = 0,nextIndex++ 进入下一个节点的判断。
2.从新集合中取得 E,然后判断旧集合中是否存在相同节点 E,可以发现不存在,此时可以 创建新节点 E。更新 lastIndex = 1,并将 E 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。
3.从新集合中取得 C,然后判断旧集合中是否存在相同节点 C,此时可以发现存在节点 C。 由于 C 在旧集合中的位置 C._mountIndex = 2,lastIndex = 1,此时 C._mountIndex > lastIndex,因此不对 C 进行移动操作。更新 lastIndex = 2,并将 C 的位置更新为新集 合中的位置,nextIndex++ 进入下一个节点的判断。
4.从新集合中取得 A,然后判断旧集合中是否存在相同节点 A,此时发现存在节点 A。由于 A 在旧集合中的位置 A._mountIndex = 0,lastIndex = 2,此时 A._mountIndex < lastIndex, 因此对 A 进行移动操作。更新 lastIndex = 2,并将 A 的位置更新为新集合中的位置, nextIndex++ 进入下一个节点的判断。
5.当完成新集合中所有节点的差异化对比后,还需要对旧集合进行循环遍历,判断是否存 在新集合中没有但旧集合中仍存在的节点,此时发现存在这样的节点 D,因此删除节点 D, 到此 diff 操作全部完成。
相关代码如下(源码路径:/v15.0.0/src/renderers/shared/reconciler/ReactMultiChild.js):
function _updateChildren(nextNestedChildrenElements, transaction, context) {
var prevChildren = this._renderedChildren;
var removedNodes = {};
var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements,
removedNodes, transaction, context);
// 如果不存在 prevChildren 和 nextChildren,则不做 diff 处理
if (!nextChildren && !prevChildren) {
return;
}
var updates = null;
var name;
// lastIndex 是 prevChildren 中最后的索引,nextIndex 是 nextChildren 中每个节点的索引
var lastIndex = 0;
var nextIndex = 0;
var lastPlacedNode = null;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild = nextChild) {
// 移动节点
updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex));
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// 通过遍历 removedNodes 删除子节点 prevChild
}
// 初始化并创建节点
updates = enqueue(updates, this._mountChildAtIndex(nextChild, lastPlacedNode, nextIndex, transaction, context));
}
nextIndex++;
lastPlacedNode = ReactReconciler.getNativeNode(nextChild);
}
// 如果父节点不存在,则将其子节点全部移除
for (name in removedNodes) {
if (removedNodes.hasOwnProperty(name)) {
updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name]));
}
}
// 如果存在更新,则处理更新队列
if (updates) {
processQueue(this, updates);
}
this._renderedChildren = nextChildren;
}
function enqueue(queue, update) {
// 如果有更新,将其存入 queue
if (update) {
queue = queue || [];
queue.push(update);
}
return queue;
}
// 处理队列的更新
function processQueue(inst, updateQueue) {
ReactComponentEnvironment.processChildrenUpdates(inst,
updateQueue,
);
}
// 移动节点
function moveChild(child, afterNode, toIndex, lastIndex) {
// 如果子节点的 index 小于 lastIndex,则移动该节点
if (child._mountIndex < lastIndex) {
return makeMove(child, afterNode, toIndex);
}
}
// 创建节点
function createChild(child, afterNode, mountImage) {
return makeInsertMarkup(mountImage, afterNode, child._mountIndex);
}
// 删除节点
function removeChild(child, node) {
return makeRemove(child, node);
}
// 卸载已经渲染的子节点
function _unmountChild(child, node) {
var update = this.removeChild(child, node); child._mountIndex = null;
return update;
}
// 通过提供的名称实例化子节点
function _mountChildAtIndex(child, afterNode, index, transaction, context) {
var mountImage = ReactReconciler.mountComponent(child, transaction, this, this._nativeContainerInfo, context);
child._mountIndex = index;
return this.createChild(child, afterNode, mountImage);
}
5.React Patch 方法
1.存在的价值:pach就是将tree diff计算出来的DOM差异队列更新到真实的DOM节点上,最终让浏览器能够渲染出更新的数据。如果没有patch,react基于Virtual DOM做再多性能优化的操作都是徒劳的,因为浏览器不认识Virtual DOM
2.实现:主要是通过遍历差异队列实现的。遍历差异队列时,通过更新类型进行相应的操作,包括:新节点的插入,已有节点的移动和删除等。
3.这里为什么可以直接依次插入节点呢?原因就是在 diff 阶段添加差异节点到差异队列时,本 身就是有序添加。也就是说,新增节点(包括 move 和 insert)在队列里的顺序就是最终真实 DOM 的顺序,因此可以直接依次根据 index 去插入节点。而且,React 并不是计算出一个差异就去执 行一次 Patch,而是计算出全部差异并放入差异队列后,再一次性地去执行 Patch 方法完成真实 DOM 的更新。
4.Patch 方 法 的 源 码 如 下 ( 源 码 路 径 : /v15.0.0/src/renderers/dom/client/utils/DOMChildren- Operations.js):
function processUpdates(parentNode, updates) {
//处理新增的节点、移动的节点以及需要移除的节点
for (var k = 0; k < updates.length; k++) {
var update = updates[k];
switch (update.type) {
// 插入新的节点
case ReactMultiChildUpdateTypes.INSERT_MARKUP:
insertLazyTreeChildAt(
parentNode,
update.content,
getNodeAfter(parentNode, update.afterNode)
);
break;
// 需要移动的节点
case ReactMultiChildUpdateTypes.MOVE_EXISTING:
moveChild(
parentNode,
update.fromNode,
getNodeAfter(parentNode, update.afterNode)
);
break;
case ReactMultiChildUpdateTypes.SET_MARKUP:
setInnerHTML(parentNode, update.content
);
break;
case ReactMultiChildUpdateTypes.TEXT_CONTENT:
setTextContent(parentNode, update.content
);
break;
// 需要删除的节点
case ReactMultiChildUpdateTypes.REMOVE_NODE:
removeChild(parentNode, update.fromNode);
break;
}
}
}
function getNodeAfter(parentNode, node) {
// 文本组件的返回格式 [open, close] comments,需要做特殊处理
if (Array.isArray(node)) {
node = node[1];
}
return node ? node.nextSibling : parentNode.firstChild;
}
// 插入新节点的操作
function insertLazyTreeChildAt(parentNode, childTree, referenceNode) {
DOMLazyTree.insertTreeBefore(parentNode, childTree, referenceNode);
}
// 移动已有节点的操作
function moveChild(parentNode, childNode, referenceNode) {
if (Array.isArray(childNode)) {
moveDelimitedText(parentNode, childNode[0], childNode[1], referenceNode);
} else {
insertChildAt(parentNode, childNode, referenceNode);
}
}
// 移除已有节点的操作
function removeChild(parentNode, childNode) {
if (Array.isArray(childNode)) {
var closingComment = childNode[1];
childNode = childNode[0];
removeDelimitedText(parentNode, childNode, closingComment); parentNode.removeChild(closingComment);
}
parentNode.removeChild(childNode);
}
// 文本组件需要去除 openingComment 和 closingComment,取得其中的 node
function moveDelimitedText(parentNode, openingComment, closingComment, referenceNode) {
var node = openingComment; while (true) {
var nextNode = node.nextSibling; insertChildAt(parentNode, node, referenceNode); if (node === closingComment) {
break;
}
node = nextNode;
}
}
function removeDelimitedText(parentNode, startNode, closingComment) {
while (true) {
var node = startNode.nextSibling; if (node === closingComment) {
// closingComment 已经被 ReactMultiChild 移除
break;
} else {
parentNode.removeChild(node);
}
}
}