前言
React16之后引入了Fiber的架构,这个架构的处理过程是非常复杂的,在正式去学习了解Fiber的处理过程之前,打算去深入了解下v15版本整个的处理过程,然后再结合Fiber要解决的问题从而更加深入的理解。本文要理解的相关原理有:
- ReactDOM.render的处理过程
- 组件的具体处理过程
- Class组件的生命周期函数的执行时机
- 事件相关处理
- setState背后更新相关逻辑
无论是最新版本还是v15版本,React.createElement的处理逻辑都是相似的,本文就不再赘述了,可以去看之前的React.createElement文章。
ReactDOM.render的处理过程
render函数的源码如下:
const ReactMount = {
render: function (nextElement, container, callback) {
return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
}
};
函数内部直接调用_renderSubtreeIntoContainer方法,该方法的处理逻辑如下:
function _renderSubtreeIntoContainer() {
...
var nextWrappedElement = React.createElement(TopLevelWrapper, {
child: nextElement
});
...
var rootComponent = ReactMount._renderNewRootComponent(
nextWrappedElement, container, shouldReuseMarkup, nextContext
);
rootComponent._renderedComponent.getPublicInstance();
}
上面的逻辑是初始化构建阶段的核心处理逻辑:
- 创建一个TopLevel容器组件,该组件的子节点是根组件
- 调用_renderNewRootComponent函数
- 调用getPublicInstance函数
_renderNewRootComponent
该函数的主要逻辑如下:
function _renderNewRootComponent() {
...
var componentInstance = instantiateReactComponent(nextElement, false);
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode, componentInstance,
container, shouldReuseMarkup, context
);
var wrapperID = componentInstance._instance.rootID;
instancesByReactRootID[wrapperID] = componentInstance;
return componentInstance;
}
其中instantiateReactComponent函数的功能是根据React元素对象创建对应的组件实例,其相关处理逻辑如下:
function instantiateReactComponent(node, shouldHaveDebugID) {
var instance;
if (typeof node === 'object') {
var element = node;
if (typeof element.type === 'string') {
instance = ReactHostComponent.createInternalComponent(element);
} else if (isInternalComponentType(element.type)) {
instance = new element.type(element);
} else {
instance = new ReactCompositeComponentWrapper(element);
}
}
...
instance._mountIndex = 0;
instance._mountImage = null;
if (Object.preventExtensions) {
Object.preventExtensions(instance);
}
return instance;
}
React元素中type属性实际上就是组件具体内容,实际上就是根据组件类型来对应实例化操作:
- 原生标签:调用ReactHostComponent.createInternalComponent处理
- 函数类型并且存在对应的原型方法的组件(实际上是内部组件):new element.type直接处理
- 其他自定义组件:new ReactCompositeComponentWrapper
ReactUpdates.batchedUpdates
ReactUpdates对象提供了一些方法,例如batchdUpdates、enqueueUpdate、flushBatchedUpdates等方法,而batchedUpdates背后实际上是调用ReactDefaultBatchingStrategy的batchedUpdates方法,该方法的处理逻辑如下:
var transaction = new ReactDefaultBatchingStrategyTransaction();
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
}
};
其中对于对于事务transaction对象的perform方法的逻辑,而该函数核心逻辑归纳如下:
var TransactionImpl = {
perform: function (method, scope, a, b, c, d, e, f) {
try {
...
this.initializeAll(0);
ret = method.call(scope, a, b, c, d, e, f);
errorThrown = false;
} finally {
try {
...
this.closeAll(0);
} finally {
this._isInTransaction = false;
}
}
return ret;
}
}
事务的处理流程就是:事务initialize方法执行、相关method方法执行、事务close方法执行。
实际上到这里就知道batchedUpdates整个的处理逻辑就是:
执行传入的batchedMountComponentIntoNode方法,利用事务Transaction机制,在该方法执行前和执行后做相关处理
batchedMountComponentIntoNode
该函数的处理逻辑具体如下:
function batchedMountComponentIntoNode(componentInstance, container, shouldReuseMarkup, context) {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
!shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement
);
transaction.perform(
mountComponentIntoNode, null, componentInstance, container,
transaction, shouldReuseMarkup, context
);
ReactUpdates.ReactReconcileTransaction.release(transaction);
}
这是出现一个新的对象ReactReconcileTransaction,从字面意思上理解是React调度事务,实际上这里的逻辑就是:
- 创建一个调度事务对象,即new ReactReconcileTransaction
- 调用这个事务的perform,实际上就是执行mountComponentIntoNode函数
- 执行调度事务的release方法
这里先不管调度事务相关的处理逻辑,主要看看mountComponentIntoNode函数,该函数的处理逻辑具体如下:
function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReuseMarkup, context) {
var markerName;
var markup = ReactReconciler.mountComponent(
wrapperInstance, transaction, null,
ReactDOMContainerInfo(wrapperInstance, container), context, 0
);
wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
ReactMount._mountImageIntoNode(
markup, container, wrapperInstance, shouldReuseMarkup, transaction
);
}
mountComponentIntoNode函数的核心逻辑就是执行两个函数:
- ReactReconciler.mountComponent
- ReactMount._mountImageIntoNode
ReactReconciler.mountComponent
该函数的最主要的逻辑实就是调用组件的mountComponent方法,因为存在一个TopLevel容器组件,首先会处理容器组件的:
var ReactCompositeComponent = {
mountComponent: function () {
...
var inst = this._constructComponent(...);
this.performInitialMount();
if (inst.componentDidMount) {
}
}
}
实际上从这里的处理逻辑就知道,实际上就是处理所有组件的挂载,因为是递归处理所以父子组件生命周期等的执行顺序如下:
父constructor -> 父componentWillMount -> 父render -> 子constructor -> 子componentWillMount -> 子render -> 子componentDidMount -> 父componentDidMount
ReactMount._mountImageIntoNode
该函数的核心处理就是插入节点到页面上,这里的逻辑就不展开了,实际上有很多细节,只要知道主要逻辑就行。
render整体逻辑总结
 在挂载阶段涉及到两个事务:
- ReactDefaultBatchingStrategyTransaction
- ReactReconcileTransaction
事件处理流程
在前面的render处理过程中会递归处理所有组件,在React中所有视图内容都是组件包括原生标签,会调用对应方法实例化生成对象。以原生标签为例,就是调用ReactDOMComponent构造函数实例化:
ReactDOMComponent.prototype.mountComponent = function() {
...
this._updateDOMProperties(null, props, transaction);
...
}
在React中所有属性、事件、子组件都是存在props属性中的,而_updateDOMProperties函数就是相关处理其中包含事件。该函数的具体处理如下:
_updateDOMProperties: function (lastProps, nextProps, transaction) {
for (propKey in lastProps) {
...
}
for (propKey in nextProps) {
if (propKey === STYLE) {
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
enqueuePutListener(this, propKey, nextProp, transaction);
}
}
...
}
}
React中事件是合成事件,而registrationNameModules中注册了所有事件的具体处理逻辑。而enqueuePutListener函数的处理逻辑具体如下:
function enqueuePutListener(inst, registrationName, listener, transaction) {
...
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
listenTo(registrationName, doc);
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
}
listenTo函数的具体处理逻辑如下:
var ReactEventListener = {
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
return EventListener.listen(
element,
handlerBaseName,
ReactEventListener.dispatchEvent.bind(null, topLevelType)
);
}
}
function listenTo() {
...
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent();
}
逻辑至此就完成了绑定的过程,这里来看看触发后事件的具体处理逻辑,即ReactEventListener.dispatchEvent。该函数的具体处理逻辑如下:
var ReactEventListener = {
dispatchEvent: function (topLevelType, nativeEvent) {
...
try {
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
}
handleTopLevel函数从源码中可知是ReactBrowserEventEmitter的方法,其具体的处理逻辑如下:
function runEventQueueInBatch(events) {
EventPluginHub.enqueueEvents(events);
EventPluginHub.processEventQueue(false);
}
handleTopLevel: function (topLevelType, targetInst) {
var events = EventPluginHub.extractEvents(topLevelType, targetInst);
runEventQueueInBatch(events);
}
实际上之后也牵扯到其他函数的处理,这边处理的最终目的就是响应对应的事件,执行自定义的程序。整个过程的处理比较复杂,其中涉及到一些细节处理,例如top开头的内部事件、SyntheticEvent的处理。
至此总结下事件处理的信息:
- React事件处理是合成事件,但是事件的触发还是要依赖浏览器DOM事件,通过addEventListener来实现监听
- 事件都是挂载到document上,而不是其定义所在的节点上
- 事件触发后内部实际上会存在一系列的内部处理,其自定义程序的执行根据开发环境和生产环境可能是函数调用 或 自定义事件分发形式
- 当触发事件后实际上整个处理流程都在处在事务机制中(这个很重要,涉及后面setState更新的处理)
setState背后的更新机制
setState用于Class组件中更新内部状态state,这个操作会触发组件视图更新,而Class组件定义规则如下:
Class Hello extends React.Component {
constructor(props) {
super(props);
this.state = { date: Date.now() };
}
render() {}
}
而React.Component组件的定义实际上也非常简单,具体逻辑如下:
function ReactComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
ReactComponent.prototype.setState = function() {};
ReactComponent.prototype.forceUpdate = function() {};
...
其中setState是其实例方法,而setState的具体逻辑如下:
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
调用组件实例的updater对应的enqueueSetState,而该属性实际上会在组件的mountComponent函数被赋值,其值实际上是ReactUpdateQueue对象:
var ReactUpdateQueue = {
enqueueSetState: function (publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
}
}
获取实例的_pendingStateQueue,将当前state数据存放到队列中,之后调用enqueueUpdate函数。该函数的逻辑具体如下:
function enqueueUpdate(component) {
ensureInjected();
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
}
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
isBatchingUpdates为false,就会执行batchedUpdates函数,实际上就是通过事务机制来执行enqueueUpdate函数;isBatchingUpdates为true,就会向dirtyComponents添加组件实例。实际上从其整个的处理逻辑看来:
无论isBatchingUpdates是什么值,都是向dirtyComponents数组中添加组件实例
而isBatchingUpdates必须是调用ReactUpdates.batchedUpdates才会设置设置为true,而其背后的处理逻辑如下:
var ReactUpdates = {
batchedUpdates: function batchedUpdates(callback, a, b, c, d, e) {
ensureInjected();
return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
}
}
batchingStrategy.batchedUpdates函数核心的功能就是通过事务机制来执行相关函数。
setState异步更新
setState可能是同步执行也可能是异步执行,其背后是通过batchingStrategy.isBatchingUpdates这个参数来控制的,而这个参数追本溯源就是看是否处于事务机制中,即是否调用了ReactUpdates.batchedUpdates方法。
setState的异步更新实际上是事务机制导致的,setState的触发可分成主动和被动两类:
- 主动是指在JavaScript中主动调用来触发
- 被动是指通过视图上事件触发导致的
在React中事件是合成事件,只要是直接在标签或组件上注册的事件都被特殊处理,实际上是从前面事件处理流程中可知:
在事件触发后,其内部处理逻辑中会调用ReactUpdates.batchedUpdates
一旦调用ReactUpdates.batchedUpdates函数,就会通过事务机制来执行事件对应的处理程序,处于事务中控制同步或异步的isBatchingUpdates就会被设置为true。以下面实例来说明会更加具体:
onClick: () => {
this.setState({ date: Date.now() })
}
当触发事件就会同步执行事件处理函数,其中setState就会触发具体处理,此时isBatchingUpdates为true,就指向将当前组件实例存放到数组dirtyComponents中,而不会立即处理。那么又是何时触发视图更新呢?实际上是在事务的结束阶段调用close方法,相关事务中会存在一个close方法是flushBatchedUpdates,而该函数中就是处理dirtyComponents的。
事务机制分为三个阶段:前期initialize函数执行、中期method执行、后期close函数。事件触发后就会处于一个事务中,事件处理函数是在事务中期阶段执行,其内部逻辑中setState会将保存组件实例到dirtyComponents数组中,而在数组在事务后期close执行时处理,这样就形成了所谓的setState的异步效果,实际上并没有任何异步效果,仅仅是执行顺序的不同而已
上面是以事件处理来看的,实际上主动触发也是如此,setState是在Class组件中使用,在前面的render的处理逻辑中可知组件的整个处理都是处于事务中,依旧是上面的事务逻辑处理过程。
setState同步更新场景
setState可以是同步的,那么什么时候是同步处理的呢?有如下两个场景:
- 在异步API中调用setState,例如setTimeout等
- 使用addEventListener注册的事件
本质上都是浏览器异步的操作,所谓的同步更新实际上也是事务机制导致的。React中事务机制都是同步执行的,当整个事务同步执行完后,才会执行异步操作。当整个事务执行完后,isBatchingUpdates参数就会被重置为false,此时异步操作执行逻辑中setState操作是执行下面逻辑:
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
batchedUpdates就会开启一个BatchingStrategy类型的事务,该事务中执行的方法是enqueueUpdate即添加组件实例到dirtyComponents,整个事务都是同步代码执行。在事务的close阶段就会处理dirtyComponents,而异步操作中setState后面的逻辑全部被阻塞了,当setState执行完成后实际上就已经完成组件的更新,后面组件相关的数据必然是最新的,setState表现出来就是同步的效果。
更新流程
当使用setState后就会触发整个相关组件的更新,在前面的setState相关逻辑中已知无论是所谓异步更新还是同步更新的对应组件都保存dirtyComponents数组中,该数组在事务后期close处理时调用flushBatchedUpdates函数来处理的,这里需要额外说明一下,React内部存在多种类型的事务,相关事务存在不同close方法。flushBatchedUpdates函数的主要处理逻辑如下:
var flushBatchedUpdates = function () {
while (dirtyComponents.length) {
...
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
...
}
};
创建一个UpdatesFlushe类型的事务,之后使用事务机制执行runBatchedUpdates函数,而runBatchedUpdates函数核心逻辑如下:
function runBatchedUpdates(transaction) {
...
dirtyComponents.sort(mountOrderComparator);
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
ReactReconciler.performUpdateIfNecessary(
component,
transaction.reconcileTransaction,
updateBatchNumber
);
...
}
}
runBatchedUpdates核心处理就是执行updateComponent来实现组件的更新逻辑,其核心处理逻辑分步概括如下:
- 生命周期函数componentWillReceiveProps执行
- 生成新的state
- 生命周期函数shouldComponentUpdate执行
- 生命周期函数componentWillUpdate执行
- _updateRenderedComponent函数调用
- 生命周期函数componentDidUpdate执行
其中对于_updateRenderedComponent函数的处理,这里总结如下:
 在更新过程中,每一次都会执行组件的render函数得到新的React元素,然后比较新旧React元素判断是否是相同元素,比较的方法是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') {
return nextType === 'string' || nextType === 'number';
} else {
return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
}
}
如果是对象类型,当新旧Element的type和key都相同就看成是相同的,需要更新(其比较的逻辑跟Vue的SameVnode有些相似)。
从上面整个的处理逻辑归总可知React v15版本中组件的更新没有使用异步API,整个过程通过递归同步处理的,如果父组件更新就会导致其整个子组件都会更新。由于JavaScript与UI渲染互斥的,当组件嵌套过深时,组件同步更新占用时间过长就会导致一些问题,比如交互响应延迟、动画掉帧等情况。为了解决v15组件同步更新的问题,才在v16版本后引入Fiber架构,通过时间切片、任务优先级调度等手段优化这个更新过程。
总结
v15中逻辑的处理相对来说还是比较清晰的,但是每一块逻辑涉及到的都比较多的,本文也是从主体流程上去看需要关注的几个点,这里简单总结下本文梳理的主要流程的逻辑,细节到文章具体模块去看:
- 通过render函数的处理过程,了解了挂载阶段的处理逻辑,其中涉及到事务机制、组件实例化过程、相关生命周期函数的执行、父子组件处理顺序(递归处理)等逻辑点
- 通过事件处理流程的简述,了解React事件处理的背后的一些关键点,例如虽然是合成事件但是还是需要通过原生DOM事件来触发等细节
- 通过setState的同步更新和异步更新的具体分析,理清其背后的原理实际上事务机制和对应参数在发挥作用,本质上并不是异步只是事务不同阶段执行导致的现象而已
- 通过分析setState的处理逻辑引出了其背后的更新过程,其中涉及到相关生命周期、更新机制等,基本理清React v15版本中递归同步批量更新组件的机制,了解这个机制的弊端从而加深对v16中Fiber架构要解决的基本问题的基本理解和认知
|