本文基于0.58.5分析React Native Reconciliation过程
Components、Elements和Instances
讲Virtual DOM之前,先讲下React Native几个核心概念和这样设计的目的。在面向对象的UI开发时,要渲染一个UI时,都要自己创建UI对象,并且管理对象引用,例如iOS上要渲染一个UIView:
UIView *view= [UIView new];
UILabel *label= [UILabel new];
label.text = @"test";
[view addSubview:label];
这种设计模式带来的问题是开发者必须自己创建、更新UI对象,当UI复杂时,维护成本会急剧增加。
React使用一种非常巧妙的设计模式来解决上面问题,UI开发时,只需要描述UI界面,引擎会根据描述自动创建具体的实例,在更新时也只需更新UI界面描述。这样开发者就从复杂的UI对象创建、更新中解放出来,开发者只需要关注UI长怎样和核心逻辑,React帮你搞定对象创建和维护,React通过Components、Elements和Instances来实现这种模式。
Elements
Element是用来描述UI的js对象,Element只有type和props两个属性,Element创建成本很低,一旦创建就不可变,Component更新到时候会创建新的Element。Element可以嵌套,React会递归解析Element直到叶子结点,这样就得到一颗Element树,这颗树就是Virtual DOM树,React通过diff等算法后把Virtual DOM渲染到屏幕,渲染过程做了很多优化。可以通过render()或者其他方法返回Element,例如:
render() {
return(
<View style = {{height:200,backgroundColor:'#999999'}}>
<Text> test </Text>
</View>
)
}
上述JSX语法最终会转换成以下Element:
{
type:View,
props: {
style:{height:200,backgroundColor:'#999999'},
children: {
type:Text,
props: {
children:'test'
}
}
}
}
Components
Component是生成Element的对象,可以是个class,也可以是简单的方法。当Component是class的时候,可以存储state和其他属性,实现复杂的逻辑;当Component是方法的时候,是不可变的Component,相当于只有render()方法的class Component。
Instances
Instance就是Component 实例,React Native开发过程不用自己管理Instance,React引擎自动创建并维护Instance,具体创建逻辑下文详细介绍。
Virtual DOM
通常所说的Virtual DOM是指相对于Real DOM的element树,Virtual DOM是最初React版本的说法,最开始React只是用在前端,引入Virtual DOM的概念是为了提升UI渲染性能,在UI变化的时候可以先比较Virtual DOM,只更新有变化的Real DOM。
Reconciliation
React之所以有这么好的渲染性能,主要是因为在UI变化的时候可以先比较Virtual DOM,只更新有变化的Real DOM,整个更新过程叫Reconciliation。
Fiber
React 16重构了Reconciliation 实现,新框架叫Fiber,Facebook团结花了两年时间实现Fiber,核心优化就是使用Vitual Stack的概念。Fiber之前更新UI的时候是通过同步递归的方式遍历Virtual DOM树,整个过程是同步的,并且在遍历结束之前无法中断,这样在动画的时候就可能导致卡顿。Fiber使用Vitual Stack的概念,把同步递归操作分解成一个个异步、可中断的操作单元,从而解决卡顿问题,并且随时可以取消不需要的操作。
UI更新
React Native UI更新主要可以分为以下两个阶段:
- Render
- Commit
Render过程计算新的element树,render()方法在这个阶段调用的;Commit过程调用diff算法,更新实际发生变化的UI。
Render 阶段核心方法调用顺序:
- componentWillReceiveProps
- getDerivedStateFromProps
- shouldComponentUpdate
- componentWillUpdate
- render()
- reconcileChildren()
Commit 阶段核心方法调用顺序:
- getSnapshotBeforeUpdate
- diff
- updateView
- componentDidUpdate
componentWillReceiveProps和componentWillUpdate已经废弃,不推荐使用了,使用getDerivedStateFromProps和getSnapshotBeforeUpdate代替
updateView方法调用RCTUIManager.updateView更新Native View。
Reconciliation源码分析
接下来以setState方法为切入点分析,分析React Native Reconciliation过程,主要分析UI更新过程,并不深入Fiber细节。
setState
_callback() {
console.log('callback');
}
_onPress1() {
this.setState(previousState => (
{ value: previousState.value+1}
), this._callback.bind(this));
}
上面代码是React Native上更新UI的最常用方法,我们知道setState是异步调用的,但state是什么时机更新?callback又什么时机调用呢?又是怎么触发Virtual DOM树和UI更新的呢?
//react.development.js:333
Component.prototype.setState = function (partialState, callback) {
!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0;
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
//ReactNativeRender-dev.js:8456
var classComponentUpdater = {
isMounted: isMounted,
enqueueSetState: function(inst, payload, callback) {
var fiber = get$1(inst);
var currentTime = requestCurrentTime();
var expirationTime = computeExpirationForFiber(currentTime, fiber);
var update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
{
warnOnInvalidCallback(callback, "setState");
}
update.callback = callback;
}
flushPassiveEffects();
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
}
...
}
React可以用在Web、Node、React Native,底层updater指向具体实现,React Native上就是classComponentUpdater。
可以看到setState最终会创建一个update结构,其中payload就是更新state的匿名方法,然后插入队列,payload和callback将在后面异步执行。
element树更新
前文说过Render过程计算新的element树,render()方法在这个阶段调用的,先看一下函数调用栈:
通过函数名可以猜测更新过程会把异步处理批量更新,这样可以提高性能,接下来分析Render过程核心方法。
performWorkOnRoot
//ReactNativeRender-dev.js:17168
function performWorkOnRoot(root, expirationTime, isYieldy) {
// Check if this is async work or sync/expired work.
if (!isYieldy) {
// Flush work without yielding.
// TODO: Non-yieldy work does not necessarily imply expired work. A renderer
// may want to perform some work without yielding, but also without
// requiring the root to complete (by triggering placeholders).
var finishedWork = root.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
completeRoot(root, finishedWork, expirationTime);
} else {
root.finishedWork = null;
// If this root previously suspended, clear its existing timeout, since
// we're about to try rendering again.
var timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
renderRoot(root, isYieldy);
finishedWork = root.finishedWork;
if (finishedWork !== null) {
// We've completed the root. Commit it.
completeRoot(root, finishedWork, expirationTime);
}
}
}
...
}
performWorkOnRoot是UI更新的入口方法,React Native上isYieldy直接传的false。每次更新的时候都会从最顶端的节点开始计算新的element树,不管是哪个节点调的setState,但没变化的节点并不会重新计算,而是直接重用。但如果父节点发生变化,则所有字节的都会进行重新计算,而不管子节点是否变化,除非子节点shouldComponentUpdate返回false,或者子节点是PureReactComponent。
renderRoot就是Render阶段入口方法,completeRoot则是Commit阶段入口方法。
workLoop
//ReactNativeRender-dev.js:16111
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until there's a higher priority event
while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
workLoop方法就是遍历整颗element树,React 16重构了Reconciliation 实现,新框架叫Fiber,Fiber使用Vitual Stack的概念,把同步递归操作分解成一个个异步、可中断的操作单元,每个操作单元就是一个节点计算过程。
performUnitOfWork就是具体节点计算,每次执行完会通过深度优先返回下一个需要执行的节点,这样就可以遍历整个节点树了。
performUnitOfWork
//ReactNativeRender-dev.js:16049
function performUnitOfWork(workInProgress) {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
var current$$1 = workInProgress.alternate;
...
var next = void 0;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
...
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
ReactCurrentOwner$2.current = null;
return next;
}
performUnitOfWork主要就是调用beginWork方法,然后更新props。
beginWork
//ReactNativeRender-dev.js:12601
function beginWork(current$$1, workInProgress, renderExpirationTime) {
var updateExpirationTime = workInProgress.expirationTime;
if (current$$1 !== null) {
var oldProps = current$$1.memoizedProps;
var newProps = workInProgress.pendingProps;
if (
oldProps === newProps &&
!hasContextChanged() &&
updateExpirationTime < renderExpirationTime
) {
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
switch (workInProgress.tag) {
...
case ClassComponent: {
var Component = workInProgress.type;
if (isContextProvider(Component)) {
pushContextProvider(workInProgress);
}
break;
}
...
return bailoutOnAlreadyFinishedWork(
current$$1,
workInProgress,
renderExpirationTime
);
}
}
// Before entering the begin phase, clear the expiration time.
workInProgress.expirationTime = NoWork;
switch (workInProgress.tag) {
...
case ClassComponent: {
var _Component2 = workInProgress.type;
var _unresolvedProps = workInProgress.pendingProps;
var _resolvedProps =
workInProgress.elementType === _Component2
? _unresolvedProps
: resolveDefaultProps(_Component2, _unresolvedProps);
return updateClassComponent(
current$$1,
workInProgress,
_Component2,
_resolvedProps,
renderExpirationTime
);
}
...
}
}
可以看到beginWork主要是两个分支,每个分支都是一个很大switch case语句,第一个分支处理节点没变化的情况,这个时候不会进行计算,第二个分支处理节点发生变化的情况。每个switch case处理不同类型的节点,这里只分析ClassComponent类型。
最终会调用updateClassComponent方法更新发生变化的节点。
updateClassComponent
//ReactNativeRender-dev.js:11457
function updateClassComponent(
current$$1,
workInProgress,
Component,
nextProps,
renderExpirationTime
) {
{
...
var instance = workInProgress.stateNode;
var shouldUpdate = void 0;
if (instance === null) {
if (current$$1 !== null) {
// An class component without an instance only mounts if it suspended
// inside a non- concurrent tree, in an inconsistent state. We want to
// tree it like a new mount, even though an empty version of it already
// committed. Disconnect the alternate pointers.
current$$1.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}
// In the initial pass we might need to construct the instance.
constructClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime
);
mountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime
);
shouldUpdate = true;
} else if (current$$1 === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime
);
} else {
shouldUpdate = updateClassInstance(
current$$1,
workInProgress,
Component,
nextProps,
renderExpirationTime
);
}
var nextUnitOfWork = finishClassComponent(
current$$1,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime
);
...
return nextUnitOfWork;
}
updateClassComponent会判断Component是否实例化,如果没有实例化的话会创建Component实例,这也是前文说的React引擎自动创建Instance的时机,如果已经实例化则调用updateClassInstance更新实例,updateClassInstance会返回该实例是否真正需要更新,并更新props和state。最后会调用finishClassComponent更新element并返回下一个计算单元。
updateClassInstance
//ReactNativeRender-dev.js:9282
// Invokes the update life-cycles and returns false if it shouldn't rerender.
function updateClassInstance(
current,
workInProgress,
ctor,
newProps,
renderExpirationTime
) {
var instance = workInProgress.stateNode;
var oldProps = workInProgress.memoizedProps;
instance.props =
workInProgress.type === workInProgress.elementType
? oldProps
: resolveDefaultProps(workInProgress.type, oldProps);
var oldContext = instance.context;
var contextType = ctor.contextType;
var nextContext = void 0;
if (typeof contextType === "object" && contextType !== null) {
nextContext = readContext$1(contextType);
} else {
var nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
nextContext = getMaskedContext(workInProgress, nextUnmaskedContext);
}
var getDerivedStateFromProps = ctor.getDerivedStateFromProps;
var hasNewLifecycles =
typeof getDerivedStateFromProps === "function" ||
typeof instance.getSnapshotBeforeUpdate === "function";
// Note: During these life-cycles, instance.props/instance.state are what
// ever the previously attempted to render - not the "current". However,
// during componentDidUpdate we pass the "current" props.
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
!hasNewLifecycles &&
(typeof instance.UNSAFE_componentWillReceiveProps === "function" ||
typeof instance.componentWillReceiveProps === "function")
) {
if (oldProps !== newProps || oldContext !== nextContext) {
callComponentWillReceiveProps(
workInProgress,
instance,
newProps,
nextContext
);
}
}
resetHasForceUpdateBeforeProcessing();
//调用setState匿名方法更新state
var oldState = workInProgress.memoizedState;
var newState = (instance.state = oldState);
var updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
processUpdateQueue(
workInProgress,
updateQueue,
newProps,
instance,
renderExpirationTime
);
newState = workInProgress.memoizedState;
}
if (
oldProps === newProps &&
oldState === newState &&
!hasContextChanged() &&
!checkHasForceUpdateAfterProcessing()
) {
// If an update was already in progress, we should schedule an Update
// effect even though we're bailing out, so that cWU/cDU are called.
if (typeof instance.componentDidUpdate === "function") {
if (
oldProps !== current.memoizedProps ||
oldState !== current.memoizedState
) {
workInProgress.effectTag |= Update;
}
}
if (typeof instance.getSnapshotBeforeUpdate === "function") {
if (
oldProps !== current.memoizedProps ||
oldState !== current.memoizedState
) {
workInProgress.effectTag |= Snapshot;
}
}
return false;
}
if (typeof getDerivedStateFromProps === "function") {
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps
);
newState = workInProgress.memoizedState;
}
var shouldUpdate =
checkHasForceUpdateAfterProcessing() ||
checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext
);
if (shouldUpdate) {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
!hasNewLifecycles &&
(typeof instance.UNSAFE_componentWillUpdate === "function" ||
typeof instance.componentWillUpdate === "function")
) {
startPhaseTimer(workInProgress, "componentWillUpdate");
if (typeof instance.componentWillUpdate === "function") {
instance.componentWillUpdate(newProps, newState, nextContext);
}
if (typeof instance.UNSAFE_componentWillUpdate === "function") {
instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
}
stopPhaseTimer();
}
if (typeof instance.componentDidUpdate === "function") {
workInProgress.effectTag |= Update;
}
if (typeof instance.getSnapshotBeforeUpdate === "function") {
workInProgress.effectTag |= Snapshot;
}
} else {
// If an update was already in progress, we should schedule an Update
// effect even though we're bailing out, so that cWU/cDU are called.
if (typeof instance.componentDidUpdate === "function") {
if (
oldProps !== current.memoizedProps ||
oldState !== current.memoizedState
) {
workInProgress.effectTag |= Update;
}
}
if (typeof instance.getSnapshotBeforeUpdate === "function") {
if (
oldProps !== current.memoizedProps ||
oldState !== current.memoizedState
) {
workInProgress.effectTag |= Snapshot;
}
}
// If shouldComponentUpdate returned false, we should still update the
// memoized props/state to indicate that this work can be reused.
workInProgress.memoizedProps = newProps;
workInProgress.memoizedState = newState;
}
// Update the existing instance's state, props, and context pointers even
// if shouldComponentUpdate returns false.
instance.props = newProps;
instance.state = newState;
instance.context = nextContext;
return shouldUpdate;
}
updateClassInstance判断节点是否需要跟新,调用以下life-cycles方法:
- componentWillReceiveProps
- getDerivedStateFromProps
- shouldComponentUpdate
- componentWillUpdate
前文已经知道调用setState时会创建一个update结构,updateClassInstance 会调用processUpdateQueue方法计算新的state,processUpdateQueue方法里面会调用setState传的匿名函数
//ReactNativeRender-dev.js:8517
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext
) {
var instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === "function") {
startPhaseTimer(workInProgress, "shouldComponentUpdate");
var shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext
);
stopPhaseTimer();
{
!(shouldUpdate !== undefined)
? warningWithoutStack$1(
false,
"%s.shouldComponentUpdate(): Returned undefined instead of a " +
"boolean value. Make sure to return true or false.",
getComponentName(ctor) || "Component"
)
: void 0;
}
return shouldUpdate;
}
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true;
}
updateClassInstance调用checkShouldComponentUpdate判断是否需要更新,checkShouldComponentUpdate实现比较简单,
- 如果Component实现shouldComponentUpdate方法,则调用shouldComponentUpdate;
- 如果是PureReactComponent,则调用shallowEqual比较props和state是否变化;
- 否则返回true
这里要注意checkShouldComponentUpdate默认返回true,所以只要父节点更新,默认就会更新所有子节点,这就是为什么可以通过shouldComponentUpdate返回false或使用PureReactComponent来提升性能。
finishClassComponent
Render阶段最后就是调用finishClassComponent方法计算新的element,并且调用reconcileChildren遍历子节点,具体代码如下:
//ReactNativeRender-dev.js:11562
function finishClassComponent(
current$$1,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime
) {
...
if (!shouldUpdate && !didCaptureError) {
// Context providers should defer to sCU for rendering
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
return bailoutOnAlreadyFinishedWork(
current$$1,
workInProgress,
renderExpirationTime
);
}
var instance = workInProgress.stateNode;
// Rerender
ReactCurrentOwner$3.current = workInProgress;
var nextChildren = void 0;
...
setCurrentPhase("render");
nextChildren = instance.render();
...
reconcileChildren(
current$$1,
workInProgress,
nextChildren,
renderExpirationTime
);
// Memoize state using the values we just used to render.
// TODO: Restructure so we never read values from the instance.
workInProgress.memoizedState = instance.state;
...
return workInProgress.child;
}
如果shouldUpdate为false,则直接重用现有节点,跟beginWork处理没变化的节点一样。
如果shouldUpdate为true,则调用render方法计算新的element,然后调用reconcileChildren遍历子节点。
completeUnitOfWork
在遍历到叶子节点后performUnitOfWork会调用completeUnitOfWork
//ReactNativeRender-dev.js:16101
function performUnitOfWork(workInProgress) {
...
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
...
}
completeUnitOfWork调用completeWork标记需要更新的节点,如果有兄弟节点则返回兄弟节点,继续遍历兄弟节点,否则标记父节点。
Commit 阶段源码分析
前文分析来Render阶段核心方法,Render阶段会生成一颗新的element树,并且生成一个Effect list,Effect list是一个线性列表,包含真正需要更新的操作,Commit 阶段则通过Effect list更新具体的UI,首先看下Commit 阶段的函数调用栈:
commitAllHostEffects
//ReactNativeRender-dev.js:15349
function commitAllHostEffects() {
while (nextEffect !== null) {
{
setCurrentFiber(nextEffect);
}
recordEffect();
var effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
var current$$1 = nextEffect.alternate;
if (current$$1 !== null) {
commitDetachRef(current$$1);
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every
// possible bitmap value, we remove the secondary effects from the
// effect tag and switch on that value.
var primaryEffectTag = effectTag & (Placement | Update | Deletion);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted
// does and isMounted is deprecated anyway so we should be able
// to kill this.
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;
// Update
var _current = nextEffect.alternate;
commitWork(_current, nextEffect);
break;
}
case Update: {
var _current2 = nextEffect.alternate;
commitWork(_current2, nextEffect);
break;
}
case Deletion: {
commitDeletion(nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
{
resetCurrentFiber();
}
}
commitAllHostEffects源码比较好理解,循环执行effect操作,effect操作可能是替换、删除、更新等
commitWork
更新操作会调用commitWork,源码如下:
//ReactNativeRender-dev.js:14628
function commitWork(current$$1, finishedWork) {
...
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// Note: We currently never use MountMutation, but useLayout uses
// UnmountMutation.
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
return;
}
case ClassComponent: {
return;
}
case HostComponent: {
var instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
var newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
var oldProps =
current$$1 !== null ? current$$1.memoizedProps : newProps;
var type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
var updatePayload = finishedWork.updateQueue;
finishedWork.updateQueue = null;
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork
);
}
}
return;
}
case HostText: {
invariant(
finishedWork.stateNode !== null,
"This should have a text node initialized. This error is likely " +
"caused by a bug in React. Please file an issue."
);
var textInstance = finishedWork.stateNode;
var newText = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
var oldText = current$$1 !== null ? current$$1.memoizedProps : newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
...
}
}
React Native上Component都是由View、Text等基础Component组成的,所以最终实际更新的是View、Text等基础Component,最后会调用commitUpdate和commitTextUpdate完成实际的更新操作。
commitUpdate
//ReactNativeRender-dev.js:4153
function commitUpdate(
instance,
updatePayloadTODO,
type,
oldProps,
newProps,
internalInstanceHandle
) {
var viewConfig = instance.viewConfig;
updateFiberProps(instance._nativeTag, newProps);
var updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
// Avoid the overhead of bridge calls if there's no update.
// This is an expensive no-op for Android, and causes an unnecessary
// view invalidation for certain components (eg RCTTextInput) on iOS.
if (updatePayload != null) {
UIManager.updateView(
instance._nativeTag, // reactTag
viewConfig.uiViewClassName, // viewName
updatePayload // props
);
}
}
commitUpdate源码比较好理解,首先调用diff判断是否需要更新,如果需要更新的话调用UIManager.updateView更新Native UI,其中UIManager.updateView是Native 暴露的module。diff方法主要是比较props是否变化:
//ReactNativeRender-dev.js:3638
function diff(prevProps, nextProps, validAttributes) {
return diffProperties(
null, // updatePayload
prevProps,
nextProps,
validAttributes
);
}
commitTextUpdate
//ReactNativeRender-dev.js:14659
//ReactNativeRender-dev.js:4145
function commitTextUpdate(textInstance, oldText, newText) {
UIManager.updateView(
textInstance, // reactTag
"RCTRawText", // viewName
{ text: newText } // props
);
}
commitTextUpdate实现比较简单,因为completeWork方法标记TextComponent的时候已经判断了text是否变化,所以直接调用UIManager.updateView。
life-cycles方法调用
在更新完UI后将调用componentDidUpdate方法和setState callback方法,具体调用栈如下:
总结
React Native Reconciliation过程比较复杂,Fiber框架把递归操作分解成一个个异步、可中断的操作单元后进一步复杂度。
Reconciliation主要可以分为以下两个阶段:
- Render
- Commit
Render阶段从根节点开始遍历element树,对于不需要更新的节点直接重用fiber node,对于需要更新的节点,调用life-cycle方法,然后调用render方法计算新的element,最后调用reconcileChildren遍历子节点。
Render阶段还会标记更新,并且生成一个Effect list,Effect list是一个线性列表,包含真正需要更新的操作。
Commit 阶段通过Effect list更新具体的UI,这阶段会调用diff,然后调用UIManager.updateView更新Native View,最后调用componentDidUpdate方法和setState callback方法。
引用
Inside Fiber: in-depth overview of the new reconciliation algorithm in React
React Fiber Architecture
React Components, Elements, and Instances
Reconciliation