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的区别进行同层比对比,如果当前层不同则替换。


React中虚拟DOM 及 生命周期

    

    带 keys 会减少对比时间形成一一映射,前提是原始虚拟 DOM 节点的 key 值和新虚拟 DOM 节点的 key值相同。不用 index 作为 key 值就是不能唯一代表一个节点。 


三、React 中生命周期函数

    生命周期函数指在某一时刻自动调用执行的函数。包含如下四个阶段。

1. Initialization

setup props and state

2. Mounting

React中虚拟DOM 及 生命周期

3. Updating

React中虚拟DOM 及 生命周期

4. Unmounting

componentWillUnmount 组件即将在页面被移除时执行,可执行一些清理方法,如事件回收或清除定时器。 


React整个生命周期如下:


React中虚拟DOM 及 生命周期


生命周期函数注意事项:

(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算法

React组件的生命周期 - 虚拟DOM - DOM Diffing算法

react 生命周期

Vue 虚拟Dom 及 部分生命周期初探

前端:react生命周期

react的生命周期