理解ReactElement和ReactClass的概念
首先让我们理解两个概念:
ReactElement
一个描述DOM节点或component实例的字面级对象。它包含一些信息,包括组件类型type
和属性props
。就像一个描述DOM节点的元素(虚拟节点)。它们可以被创建通过React.createElement
方法或jsx
写法
分为DOM Element
和Component Elements
两类:
DOM Elements
当节点的type属性为字符串时,它代表是普通的节点,如div,span
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}
Component Elements
当节点的type属性为一个函数或一个类时,它代表自定义的节点
class Button extends React.Component {
render() {
const { children, color } = this.props;
return {
type: 'button',
props: {
className: 'button button-' + color,
children: {
type: 'b',
props: {
children: children
}
}
}
};
}
}
// Component Elements
{
type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}
ReactClass
ReactClass是平时我们写的Component组件(类或函数),例如上面的Button
类。ReactClass实例化后调用render方法可返回DOM Element
。
react渲染过程
过程理解:
// element是 Component Elements
ReactDOM.render({
type: Form,
props: {
isSubmitted: false,
buttonText: 'OK!'
}
}, document.getElementById('root'));
调用
React.render
方法,将我们的element
根虚拟节点渲染到container
元素中。element
可以是一个字符串文本元素,也可以是如上介绍的ReactElement
(分为DOM Elements, Component Elements)。根据
element
的类型不同,分别实例化ReactDOMTextComponent
,ReactDOMComponent
,ReactCompositeComponent
类。这些类用来管理ReactElement
,负责将不同的ReactElement
转化成DOM(mountComponent方法),负责更新DOM(receiveComponent方法,updateComponent方法, 如下会介绍)等。ReactCompositeComponent
实例调用mountComponent
方法后内部调用render
方法,返回了DOM Elements
。再对如图的步骤2️⃣递归。
react更新机制
每个类型的元素都要处理好自己的更新:
自定义元素的更新,主要是更新render出的节点,做甩手掌柜交给render出的节点的对应component去管理更新。
text节点的更新很简单,直接更新文案。
-
浏览器基本元素的更新,分为两块:
先是更新属性,对比出前后属性的不同,局部更新。并且处理特殊属性,比如事件绑定。
然后是子节点的更新,子节点更新主要是找出差异对象,找差异对象的时候也会使用上面的shouldUpdateReactComponent来判断,如果是可以直接更新的就会递归调用子节点的更新,这样也会递归查找差异对象。不可直接更新的删除之前的对象或添加新的对象。之后根据差异对象操作dom元素(位置变动,删除,添加等)。
第一步:调用this.setState
ReactClass.prototype.setState = function(newState) {
//this._reactInternalInstance是ReactCompositeComponent的实例
this._reactInternalInstance.receiveComponent(null, newState);
}
第二步:调用内部receiveComponent方法
这里主要分三种情况,文本元素,基本元素,自定义元素。
自定义元素:
receiveComponent方法源码
// receiveComponent方法
ReactCompositeComponent.prototype.receiveComponent = function(nextElement, transaction, nextContext) {
var prevElement = this._currentElement;
var prevContext = this._context;
this._pendingElement = null;
this.updateComponent(
transaction,
prevElement,
nextElement,
prevContext,
nextContext
);
}
updateComponent方法源码
// updateComponent方法
ReactCompositeComponent.prototype.updateComponent = function(
transaction,
prevParentElement,
nextParentElement,
prevUnmaskedContext,
nextUnmaskedContext
) {
// 简写.....
// 不是state更新而是props更新
if (prevParentElement !== nextParentElement) {
willReceive = true;
}
if (willReceive && inst.componentWillReceiveProps) {
// 调用生命周期componentWillReceiveProps方法
}
// 是否更新元素
if (inst.shouldComponentUpdate) {
// 如果提供shouldComponentUpdate方法
shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
} else {
if (this._compositeType === CompositeTypes.PureClass) {
// 如果是PureClass,浅层对比props和state
shouldUpdate =
!shallowEqual(prevProps, nextProps) ||
!shallowEqual(inst.state, nextState);
}
}
if (shouldUpdate) {
// 更新元素
this._performComponentUpdate(
nextParentElement,
nextProps,
nextState,
nextContext,
transaction,
nextUnmaskedContext
);
} else {
// 不更新元素,但仍然设置props和state
this._currentElement = nextParentElement;
this._context = nextUnmaskedContext;
inst.props = nextProps;
inst.state = nextState;
inst.context = nextContext;
}
// .......
}
内部_performComponentUpdate方法源码
// 内部_updateRenderedComponentWithNextElement方法
ReactCompositeComponent.prototype._updateRenderedComponentWithNextElement = function() {
// 判定两个element需不需要更新
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
// 如果需要更新,就继续调用子节点的receiveComponent的方法,传入新的element更新子节点。
ReactReconciler.receiveComponent(
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context)
);
} else {
// 卸载之前的子节点,安装新的子节点
var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
ReactReconciler.unmountComponent(
prevComponentInstance,
safely,
false /* skipLifecycle */
);
var nodeType = ReactNodeTypes.getType(nextRenderedElement);
this._renderedNodeType = nodeType;
var child = this._instantiateReactComponent(
nextRenderedElement,
nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */
);
this._renderedComponent = child;
var nextMarkup = ReactReconciler.mountComponent(
child,
transaction,
this._hostParent,
this._hostContainerInfo,
this._processChildContext(context),
debugID
);
}
shouldUpdateReactComponent函数源码
function shouldUpdateReactComponent(prevElement, nextElement){
var prevEmpty = prevElement === null || prevElement === false;
var nextEmpty = nextElement === null || nextElement === false;
if (prevEmpty || nextEmpty) {
return prevEmpty === nextEmpty;
}
var prevType = typeof prevElement;
var nextType = typeof nextElement;
if (prevType === 'string' || prevType === 'number') {
// 如果先前的ReactElement对象类型是字符串或数字,新的ReactElement对象类型也是字符串或数字,则需要更新,新的ReactElement对象类型是对象,则不应该更新,直接替换。
return (nextType === 'string' || nextType === 'number');
} else {
// 如果先前的ReactElement对象类型是对象,新的ReactElement对象类型也是对象,并且标签类型和key值相同,则需要更新
return (
nextType === 'object' &&
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
);
}
}
文本元素:
receiveComponent方法源码
ReactDOMTextComponent.prototype.receiveComponent(nextText, transaction) {
//跟以前保存的字符串比较
if (nextText !== this._currentElement) {
this._currentElement = nextText;
var nextStringText = '' + nextText;
if (nextStringText !== this._stringText) {
this._stringText = nextStringText;
var commentNodes = this.getHostNode();
// 替换文本元素
DOMChildrenOperations.replaceDelimitedText(
commentNodes[0],
commentNodes[1],
nextStringText
);
}
}
}
基本元素:
receiveComponent方法源码
ReactDOMComponent.prototype.receiveComponent = function(nextElement, transaction, context) {
var prevElement = this._currentElement;
this._currentElement = nextElement;
this.updateComponent(transaction, prevElement, nextElement, context);
}
updateComponent方法源码
ReactDOMComponent.prototype.updateComponent = function(transaction, prevElement, nextElement, context) {
// 略.....
//需要单独的更新属性
this._updateDOMProperties(lastProps, nextProps, transaction, isCustomComponentTag);
//再更新子节点
this._updateDOMChildren(
lastProps,
nextProps,
transaction,
context
);
// ......
}
this._updateDOMChildren
方法内部调用diff算法,请看下一节........
react Diff算法
diff算法源码
_updateChildren: function(nextNestedChildrenElements, transaction, context) {
var prevChildren = this._renderedChildren;
var removedNodes = {};
var mountImages = [];
// 获取新的子元素数组
var nextChildren = this._reconcilerUpdateChildren(
prevChildren,
nextNestedChildrenElements,
mountImages,
removedNodes,
transaction,
context
);
if (!nextChildren && !prevChildren) {
return;
}
var updates = null;
var name;
var nextIndex = 0;
var lastIndex = 0;
var nextMountIndex = 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) {
// 同一个引用,说明是使用的同一个component,所以我们需要做移动的操作
// 移动已有的子节点
// NOTICE:这里根据nextIndex, lastIndex决定是否移动
updates = enqueue(
updates,
this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)
);
// 更新lastIndex
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// 更新component的.mountIndex属性
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
// 更新lastIndex
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
}
// 添加新的子节点在指定的位置上
updates = enqueue(
updates,
this._mountChildAtIndex(
nextChild,
mountImages[nextMountIndex],
lastPlacedNode,
nextIndex,
transaction,
context
)
);
nextMountIndex++;
}
// 更新nextIndex
nextIndex++;
lastPlacedNode = ReactReconciler.getHostNode(nextChild);
}
// 移除掉不存在的旧子节点,和旧子节点和新子节点不同的旧子节点
for (name in removedNodes) {
if (removedNodes.hasOwnProperty(name)) {
updates = enqueue(
updates,
this._unmountChild(prevChildren[name], removedNodes[name])
);
}
}
}
react的优点与总结
优点
虚拟节点。在UI方面,不需要立刻更新视图,而是生成虚拟DOM后统一渲染。
组件机制。各个组件独立管理,层层嵌套,互不影响,react内部实现的渲染功能。
差异算法。根据基本元素的key值,判断是否递归更新子节点,还是删除旧节点,添加新节点。
总结
想要更好的利用react的虚拟DOM,diff算法的优势,我们需要正确的优化、组织react页面。例如将一个页面render的ReactElement
节点分解成多个组件。在需要优化的组件手动添加 shouldComponentUpdate
来避免不需要的 re-render
。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。