React中虚拟DOM 及 生命周期
Posted 小小小小小莹
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React中虚拟DOM 及 生命周期相关的知识,希望对你有一定的参考价值。
一、React 中虚拟 DOM 生成 及 更新
例如:生成 DOM 结构
<div id="abc"><span>hello world</span></div>
之后更改 span 标签中内容,
<div id="abc"><span>ok bye</span></div>
将会执行如下过程。
1. state 数据
2. JSX 模板
3. 生成虚拟 DOM(它是一个 JS 对象,用它来描述真实 DOM )
['div', {id: 'abc'}, ['span', {}, 'hello world']]
4. 根据虚拟 DOM 数据 + 模板结合,生成真实 DOM 来显示。(降低了性能)
<div id="abc"><span>hello world</span></div>
5. state 发生变化
6. 数据 + 模板结合生成新的虚拟 DOM
['div', {id: 'abc'}, ['span', {}, 'ok bye']]
7. 比较原始虚拟 DOM 和新的虚拟 DOM 的区别,找到区别是 span 中内容不同。(提升了性能)
8. 直接操作 DOM,改变 span 中内容。(提升了性能)
* 提升性能的原因:减少 DOM 的生成和比较,以 JS 对象替换。
* 虚拟 DOM 带来的好处:性能提升;跨端应用得以实现React Native(原生应用无html结构)
二、React 中虚拟 DOM 的 Diff 算法
当 state 发生改变,比较原始虚拟 DOM 和新虚拟 DOM的区别进行同层比对比,如果当前层不同则替换。
带 keys 会减少对比时间形成一一映射,前提是原始虚拟 DOM 节点的 key 值和新虚拟 DOM 节点的 key值相同。不用 index 作为 key 值就是不能唯一代表一个节点。
三、React 中生命周期函数
生命周期函数指在某一时刻自动调用执行的函数。包含如下四个阶段。
1. Initialization
setup props and state
2. Mounting
3. Updating
4. Unmounting
componentWillUnmount 组件即将在页面被移除时执行,可执行一些清理方法,如事件回收或清除定时器。
React整个生命周期如下:
生命周期函数注意事项:
(1) shouldComponentUpdate 是一个特别的方法,它接收更新的 props 和state,可增加必要的条件,让其在需要时更新,不需要时不更新。因此,当方法返回 false 时,组件不再向下执行。默认返回 true 。
(2) 不能在 componentWillUpdate 和 shouldComponetUpdate 中执行 setState 。
首先看下 setState的源码:
ReactComponent.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
这其中该方法传入两个参数 partialState 是新的 state 值,callBack 后者是回调函数,updater 是在构造函数中定义的一个变量,从方法名 enqueueSetState 中可以明白,传入的新的 state 被 enqueue 推入了一个栈中,并不是立即更新,随后继续跟踪代码。
enqueueSetState: function(publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState'
);
if (!internalInstance) {
return;
}
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue) = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
}
getInternalInstanceReadyForUpdate 方法获取了当前组件对象,并将其赋给 internalInstance。接下来判断当前组件对象的 state 是否存在更新队列,若存在则把新的 state 值 push 到队列中,若不存在,则创建一个空的新队列。
function enqueueUpdate(component) {
ensureInjected();
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
}
这里的代码也很好理解,首先 ensureInjected 方法检查当前运行的代码是否处在一个事务(reconcile transaction)中,若不是则会抛出错误。且若 batchingStrategy.isBatchingUpdates 为 false(可以简单理解为当前不是在一个批处理流程中),则进行 batchedUpdates(批量更新),若为 true,则推入 dirtyComponents 中,接下来跟踪并看下 batchingStrategy 的源码。
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
/*
* Call the provided function in a context within which class to 'setState'
* and friends are batched such that components aren't updated unnecessarily
*/
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// The code is written this way to avoid extra allocations
if (alreadyBatchUpdates) {
callback(a, b, c, d, e);
} else {
transaction.perform(callback, null, a, b, c, d, e);
}
}
};
至于为什么要做 batchUpdates(批量更新),是“为了避免组件被不必要地更新”。React 内部存在着"状态机"这个概念,也就是说当组件处于不同的状态时,所执行的逻辑是不同的。具体来说,React 以事务+状态的方法来对组件进行更新。
下面这张图来源于 React 对事务的解释:
事务就是将需要执行的方法使用 wrapper 封装起来,再通过事务提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法,执行完 perform 之后(即执行method 方法后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper。从上图中可以看出,事务支持多个 wrapper 叠加。
到实现上,事务提供了一个 mixin 方法供其他模块实现自己需要的事务。而要使用事务的模块,除了需要把 mixin 混入自己的事务实现中外,还要额外实现一个抽象的 getTransactionWrappers 接口。这个接口用来获取所有需要封装的前置方法(initialize)和收尾方法(close), 因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。
为什么 React 要引入 Transaction 事务这个概念?对于 React 来说,主要有以下几个应用场景:
1. 在 Reconciliation 调和之前/之后保留输入选择范围。 即使出现意外错误也可以恢复这个选择。
2. 在重新排列DOM时停用事件,同时确保事后事件能被重新激活。
以上为 React 为什么引入Transaction事务,接下来看下ReactDefaultBatchingStrategy 中的 Transaction 是如何实现的。
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
function ReactDefaultBatchingStrategyTransaction() {
this.reinitializeTransaction();
}
Object.assign(
ReactDefaultBatchingStrategyTransaction.prototype,
Transaction.Mixin,
{
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
}
}
);
可以看到这里定义了2个 wrapper,其中 RESET_BATCHED_UPDATES 负责在 close 阶段重置 ReactDefaultBatchingStrategy 的 isBatchingUpdates 为 false 。而 FLUSH_BATCHED_UPDATES 负责在 close 执行 flushBatchedUpdates ,在这个方法里包含了 Virtual DOM 到真实 DOM 的映射等其他操作,且此方法会清空 dirtyComponents 数组并执行 runBatchedUpdate 方法。
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
dirtyComponents.sort(mountOrderComparator);
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
var markerName;
if (ReactFeatureFlags.logTopLevelRenders) {
var namedComponent = component;
if (component._currentElement.props === component._renderedComponent._currentElement) {
namedComponent = component._renderedComponent;
}
}
ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
}
}
}
}
这里 dirtyComponents 数组会进行一个排序操作,这里因为通常情况下,父组件更新后,子组件也会随之更新,所以这里进先进行排序,使得子组件在父组件之前被更新,同时将 setState 中传入的回调函数存入 callbackQueue 队列中,且 performUpdateIfNecessary 方法中执行了 updateComponent 方法,接下来看一下这个方法都做了什么。
updateComponent: function(
transaction,
prevParentElement,
nextParentElement,
prevUnmaskedContext,
nextUnmaskedContext,
) {
var inst = this._instance;
invariant(
inst != null,
'Attempted to update component `%s` that has already been unmounted ' +
'(or failed to mount).',
this.getName() || 'ReactCompositeComponent',
);
var willReceive = false;
var nextContext;
// Determine if the context has changed or not
if (this._context === nextUnmaskedContext) {
nextContext = inst.context;
} else {
nextContext = this._processContext(nextUnmaskedContext);
willReceive = true;
}
var prevProps = prevParentElement.props;
var nextProps = nextParentElement.props;
// Not a simple state update but a props update
if (prevParentElement !== nextParentElement) {
willReceive = true;
}
// An update here will schedule an update but immediately set
// _pendingStateQueue which will ensure that any state updates gets
// immediately reconciled instead of waiting for the next batch.
if (willReceive && inst.componentWillReceiveProps) {
if (__DEV__) {
measureLifeCyclePerf(
() => inst.componentWillReceiveProps(nextProps, nextContext),
this._debugID,
'componentWillReceiveProps',
);
} else {
inst.componentWillReceiveProps(nextProps, nextContext);
}
}
var nextState = this._processPendingState(nextProps, nextContext);
var shouldUpdate = true;
if (!this._pendingForceUpdate) {
if (inst.shouldComponentUpdate) {
if (__DEV__) {
shouldUpdate = measureLifeCyclePerf(
() => inst.shouldComponentUpdate(nextProps, nextState, nextContext),
this._debugID,
'shouldComponentUpdate',
);
} else {
shouldUpdate = inst.shouldComponentUpdate(
nextProps,
nextState,
nextContext,
);
}
} else {
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate =
!shallowEqual(prevProps, nextProps) ||
!shallowEqual(inst.state, nextState);
}
}
}
if (__DEV__) {
warning(
shouldUpdate !== undefined,
'%s.shouldComponentUpdate(): Returned undefined instead of a ' +
'boolean value. Make sure to return true or false.',
this.getName() || 'ReactCompositeComponent',
);
}
this._updateBatchNumber = null;
if (shouldUpdate) {
this._pendingForceUpdate = false;
// Will set `this.props`, `this.state` and `this.context`.
this._performComponentUpdate(
nextParentElement,
nextProps,
nextState,
nextContext,
transaction,
nextUnmaskedContext,
);
} else {
// If it's determined that a component should not update, we still want
// to set props and state but we shortcut the rest of the update.
this._currentElement = nextParentElement;
this._context = nextUnmaskedContext;
inst.props = nextProps;
inst.state = nextState;
inst.context = nextContext;
}
}
接下来看一下这个_processPendingState方法:
_processPendingState: function (props, context) {
var inst = this._instance;
var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;
this._pendingReplaceState = false;
this._pendingStateQueue = null;
if (!queue) {
return inst.state;
}
if (replace && queue.length === 1) {
return queue[0];
}
var nextState = _assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;
}
这个函数对 state 主要做了以下几件事:
1. 如果更新队列为null,那么返回原来的state;
2. 如果更新队列有一个更新值,那么返回更新值;
3. 如果更新队列有多个更新,那么通过for循环将它们合并;
也就是说,在一个生命周期所有的state变化都会被合并,并统一处理。performUpdate 这个函数的功能就是在更新组件前后分别执行 componentWillUpdate 和 componentDidUpdate 。而在负责更新的_updateRenderedComponent 函数中,我们根据传入的新旧组件信息判断是否进行更新。若返回值为 true,执行旧组件的更新,否则的话执行旧组件的卸载和新组件的挂载。
整个流程图如下:
组件更新时,state 值还没有合并,则 this._pendingStateQueue 为 true,使得 setState 会再次调用 updateComponent,随后继续调用 componentWillUpdate 和 shouldComponetUpdate 方法,导致死循环,而正常情况下,已经更新过的组件不会进入再次更新的流程。
(3) 如果组件是由父组件更新 props 更新的,那么 shouldComponentUpdate之前会执行 componentReceiveProps,此方法可作为 React 在传入 props 之后渲染之前,setState 的机会并不会二次渲染。
以上是关于React中虚拟DOM 及 生命周期的主要内容,如果未能解决你的问题,请参考以下文章
ReactReact全家桶React 生命周期+虚拟DOM+Diff算法