本文章主要是通过阅读深入react技术栈这本书而积累总结读代码而来。所以文章中很多图和描述都来自此书。
react组件究竟是什么
- 虚拟组件所需的元素只有如下:
-
标签名
-
节点属性,包含样式、属性、事件等 子节点
-
标识 id 示例代码如下:
{ // 标签名 tagName: 'div', // 属性 properties: { // 样式 style: {} }, // 子节点 children: [], // 唯一标识 key: 1 }
-
1 创建react元素
creatElement创建虚拟dom节点元素,当使用 React 创建组件时,首先会调用 instantiateReactComponent,这是初始化组件的入口 函数,它通过判断 node 类型来区分不同组件的入口。
1)DOM组件
ReactDOMComponent 针对 Virtual DOM 标签的处理主要分为以下两个部分:
- 属性的更新,包括更新样式、更新属性、处理事件等;更新属性会先删除不需要的旧属性,再更新新的属性内容。
- 子节点的更新,包括更新内容、更新子节点,此部分涉及 diff 算法。先删除不需要的子节点和内容,再更新子节点和内容。
2)自定义组件
ReactCompositeComponent 自定义组件实现了一整套 React 生命周期和 setState 机制,因此自定义组件是在生命周期的环境中进行更新属性、内容和子节点的操作。这些更新操作与 ReactDOMComponent 的操作类似。 生命周期的内容可以看下面小节。
react15的生命周期以及都干了啥
- 生命周期执行顺序如下:
自定义组件(ReactCompositeComponent)的生命周期主要通过 3 个阶段进行管理—— MOUNTING、RECEIVE_PROPS 和 UNMOUNTING
1.createClass创建组件
createClass是自定义组件的入口,负责getDefaultProps。
2.阶段一:Mounting
mountComponent 负责管理生命周期中的 getInitialState、componentWillMount、render 和componentDidMount。 若存在 componentWillMount,则执行。如果此时在 componentWillMount 中调用 setState 方法,是不会触发 re-render的,而是会进行 state 合并(是和组件的state合并,而不是只更新到队列),因此 componentWillMount 中 的 this.state 并不是最新的,在 render 中才可以获取更新后的 this.state。
由于递归的特性,父组件的 componentWillMount 在其子组件的 componentWillMount 之前调用,而父组件的 componentDidMount 在其子组件的 componentDidMount 之后调用。
无状态组件没有更新队列只是专注于渲染。
3. 阶段二:RECEIVE_PROPS
- 注意:禁止在componentWillUpdate以及shouldComponentUpdate中setstate,会导致循环,直至浏览器内存耗光而崩溃。
4.阶段三:UNMOUNTING
unmountComponent 负责管理生命周期中的 componentWillUnmount。
无状态组件
无状态组件没有状态,没有生命周期,只是简单地接受 props 渲染生成 DOM 结构,是一个 纯粹为渲染而生的组件。由于无状态组件有简单、便捷、高效等诸多优点,所以如果可能的话, 请尽量使用无状态组件。
//无状态组件使用
const HelloWorld = (props) => <div>{props.name}</div>;
ReactDOM.render(<HelloWorld name="Hello World!" />, App);
//render 函数和 shouldConstruct 函数的代码如下(源码路径:/v15.0.0/src/renderers/shared/re-conciler/ReactCompositeComponent.js):
// 无状态组件只有一个 render 函数
StatelessComponent.prototype.render = function() {
var Component = ReactInstanceMap.get(this)._currentElement.type;
// 没有 state 状态
var element = Component(this.props, this.context, this.updater);
warnIfInvalidElement(Component, element);
return element;
};
function shouldConstruct(Component) {
return Component.prototype;
Component.prototype.isReactComponent;
}
总周期
setState后发生了什么
setState是否异步
首先,setstate后,会更新一个更新队列的内容,会在某一个生命周期内,合并这个更新队列内容和inst.state(这里的inst就是react component)。在componentWillMount中setstate,不会立即re-render,inst.state = this._processPendingState(inst.props,inst.context).
React 初学者常会写出 this.state.value = 1 这样的代码,这是完全错误的写法。 setState 通过一个队列机制实现 state 更新。当执行 setState 时,会将需要更新的 state 合并 后放入状态队列,而不会立刻更新 this.state。 队列机制可以高效地批量更新 state。如果不通过 setState 而直接修改 this.state 的值,那么该 state 将不会被放入状态队列中,当下次调用 setState 并对状态队列进行合并时,将会忽略之前直接被修改的 state,而造成无法预知的错误。
// 将新的 state 合并到状态更新队列中
var nextState = this._processPendingState(nextProps, nextContext);
// 根据更新队列和 shouldComponentUpdate 的状态来判断是否需要更新组件
var shouldUpdate =this._pendingForceUpdate || !inst.shouldComponentUpdate ||
inst.shouldComponentUpdate(nextProps, nextState, nextContext);
所以setstate其实并不是异步,只是由于一些react事件机制,让其表现异步。
setState流程
setstate会触发enqueuesetstate,会把需要更新的内容push到一个queue中,并调用enqueueUpdate,enqueueUpdate中会判断是否处在批量更新模式,如果 isBatchingUpdates 为 true,则对所有队列中的更新执行 batchedUpdates 方法,否则只 把当前组件(即调用了 setState 的组件)放入 dirtyComponents 数组中。
ReactComponent.prototype.setState = function (partialState, callback) {
//``其他特殊情况代码
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback);
}
};
enqueueSetState: function (publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
},
function enqueueUpdate(component) {
ensureInjected();
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
}
batchedUpdates 方法中有一个 transaction.perform 调用,这就需要说一下事务,后面小节会讲到。
setState循环调用风险
其中updateComponent代码: 会合并state
事务
事务就是将需要执行的方法使用 wrapper 封装起来,再通过事务提供的 perform 方法执行。 而在 perform 之前,先执行所有 wrapper 中的 initialize 方法,执行完 perform 之后(即执行 method 方法后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper。从 图3-16中可以看出,事务支持多个 wrapper 叠加。
import React, { Component } from 'react';
class Example extends Component { constructor() {
super(); this.state = {
val: 0 };
}
componentDidMount() {
this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 1 次输出
this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 2 次输出
setTimeout(() => {
this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 3 次输出
this.setState({val: this.state.val + 1});
console.log(this.state.val);// 第 4 次输出
}, 0);
}
render() {
return null;
} }
4 次 console.log 打印出来的 val 分别是:0、0、2、3。
可以观察下图直接调用setState和setTimeout中的调用栈,可以看出 下面重点看看第一类 setState 的调 用栈,有没有发现什么?没错,就是 batchedUpdates 方法,原来早在 setState 调用前,已经处 于 batchedUpdates 执行的事务中了。 那这次 batchedUpdate 方法,又是谁调用的呢?让我们往前再追溯一层,原来是 ReactMount.js 中的_renderNewRootComponent 方法。也就是说,整个将 React 组件渲染到 DOM 中的过程就处于 一个大的事务中。 接下来的解释就顺理成章了,因为在 componentDidMount 中调用 setState 时,batchingStrategy 的 isBatchingUpdates 已经被设为 true,所以两次 setState 的结果并没有立即生效,而是被放进 了 dirtyComponents 中。这也解释了两次打印 this.state.val 都是 0 的原因。
react15中diff
diff操作分为tree diff、component diff、element diff。
1 tree diff
React 通过 updateDepth对 Virtual DOM 树进行层级控制,只会对相同层级的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
/**
* Updates the rendered children with new children.
*
* @param {?object} nextNestedChildrenElements Nested child element maps.
* @param {ReactReconcileTransaction} transaction
* @internal
*/
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();
}
}
}
},
/**
* Improve performance by isolating this hot code path from the try/catch
* block in `updateChildren`.
*
* @param {?object} nextNestedChildrenElements Nested child element maps.
* @param {ReactReconcileTransaction} transaction
* @final
* @protected
*/
_updateChildren: function (nextNestedChildrenElements, transaction, context) {
var prevChildren = this._renderedChildren;
var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, transaction, context);
this._renderedChildren = nextChildren;
if (!nextChildren && !prevChildren) {
return;
}
var name;
// `nextIndex` will increment for each child in `nextChildren`, but
// `lastIndex` will be the last index visited in `prevChildren`.
var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
this.moveChild(prevChild, nextIndex, lastIndex);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
// Update `lastIndex` before `_mountIndex` gets unset by unmounting.
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
this._unmountChild(prevChild);
}
// The child must be instantiated before it's mounted.
this._mountChildByNameAtIndex(nextChild, name, nextIndex, transaction, context);
}
nextIndex++;
}
// Remove children that are no longer present.
for (name in prevChildren) {
if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
this._unmountChild(prevChildren[name]);
}
}
},
2 component diff
当组件D变为组件G时,即使这两个组件结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除组件 D,重新创建组件 G 及其子节 点。虽然当两个组件是不同类型但结构相似时,diff 会影响性能,但正如 React 官方博客所言: 不同类型的组件很少存在相似 DOM 树的情况,因此这种极端因素很难在实际开发过程中造成重 大的影响。
3 element diff
当节点发生了后移才移动,否则不移动位置。
function enqueueInsertMarkup(parentID, markup, toIndex) {
// NOTE: Null values reduce hidden classes.
updateQueue.push({
parentID: parentID,
parentNode: null,
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
markupIndex: markupQueue.push(markup) - 1,
content: null,
fromIndex: null,
toIndex: toIndex
});
}
/**
* Enqueues moving an existing element to another index.
*
* @param {string} parentID ID of the parent component.
* @param {number} fromIndex Source index of the existing element.
* @param {number} toIndex Destination index of the element.
* @private
*/
function enqueueMove(parentID, fromIndex, toIndex) {
// NOTE: Null values reduce hidden classes.
updateQueue.push({
parentID: parentID,
parentNode: null,
type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
markupIndex: null,
content: null,
fromIndex: fromIndex,
toIndex: toIndex
});
}
/**
* Enqueues removing an element at an index.
*
* @param {string} parentID ID of the parent component.
* @param {number} fromIndex Index of the element to remove.
* @private
*/
function enqueueRemove(parentID, fromIndex) {
// NOTE: Null values reduce hidden classes.
updateQueue.push({
parentID: parentID,
parentNode: null,
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
markupIndex: null,
content: null,
fromIndex: fromIndex,
toIndex: null
});
}
/**
* Enqueues setting the markup of a node.
*
* @param {string} parentID ID of the parent component.
* @param {string} markup Markup that renders into an element.
* @private
*/
function enqueueSetMarkup(parentID, markup) {
// NOTE: Null values reduce hidden classes.
updateQueue.push({
parentID: parentID,
parentNode: null,
type: ReactMultiChildUpdateTypes.SET_MARKUP,
markupIndex: null,
content: markup,
fromIndex: null,
toIndex: null
});
}
/**
* Enqueues setting the text content.
*
* @param {string} parentID ID of the parent component.
* @param {string} textContent Text content to set.
* @private
*/
function enqueueTextContent(parentID, textContent) {
// NOTE: Null values reduce hidden classes.
updateQueue.push({
parentID: parentID,
parentNode: null,
type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
markupIndex: null,
content: textContent,
fromIndex: null,
toIndex: null
});
}
在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作。当节点数量过大或更新操作过于频繁时,这在一定程度上会影响 React 的渲染性能。
主要的更新流程是,setstate后,会在willmount或者willupdate函数最后合并更新队列,然后updatecomponent会进行diff操作,render阶段里,会引起receivecomponent阶段对属性进行更新。
如下图:
React patch
这里为什么可以直接依次插入节点呢?原因就是在 diff 阶段添加差异节点到差异队列时,本 身就是有序添加。也就是说,新增节点(包括 move 和 insert)在队列里的顺序就是最终真实 DOM 的顺序,因此可以直接依次根据 index 去插入节点。而且,React 并不是计算出一个差异就去执 行一次 Patch,而是计算出全部差异并放入差异队列后,再一次性地去执行 Patch 方法完成真实 DOM 的更新。