深入浅出react---2.解读react源码

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 节点的属性和事件

  1. 如果存在事件,则针对当前的节点添加事件代理,即调用 enqueuePutListener(this, propKey, propValue, transaction)。
  2. 如果存在样式,首先会对样式进行合并操作Object.assign({}, props.style),然后通过CSSPropertyOperations.createMarkupForStyles(propValue, this) 创建样式。
  3. 通过DOMPropertyOperations.createMarkupForProperty(propKey, propValue)创建属性。
  4. 通过DOMPropertyOperations.createMarkupForID(this._domID)创建唯一标识。
  5. 如果存在事件,则针对当前的节点添加事件代理,即调用 enqueuePutListener(this, propKey, propValue, transaction)。
  6. 如果存在样式,首先会对样式进行合并操作Object.assign({}, props.style),然后通过CSSPropertyOperations.createMarkupForStyles(propValue, this) 创建样式。
  7. 通过DOMPropertyOperations.createMarkupForProperty(propKey, propValue)创建属性。
  8. 通过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. 策略1.Web UI 中 DOM节点跨层级的移动操作特别少,可以忽略不计。
  2. 策略2.拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  3. 策略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);
    }
  }
}

6.react-router原理

7.React Fiber

https://zhuanlan.zhihu.com/p/26027085

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值