转载自前端备忘录-江湖术士

这部分会非常枯燥,如果不想看的可以不看,直接给结论到就行,那就是——

React DDD 并不需要你考虑性能问题

除了惰性初始化

小字也注意一下,要严谨一些

那好,我们开始吧

首先,我们要知道,class 下的 React 协调调度和 function 下的逻辑是不同的,不要拿着 class 的逻辑 说 function,这样没有意义,这属于历史问题

这不单是给 React 程序员说的,也是给其他平台程序员说的,hooks 发布之后,这两种写法其实是势同水火,官方只是为了保证平滑升级而已(ng 暴力升级就不会有这方面考量,有利有弊)

首先,在 reconcile 源码的 beginWork 入口处,你就能看到 updateFunctionComponent 和 updateClassComponent 的不同:

https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberBeginWork.new.js

updateFunctionComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function updateFunctionComponent(
current,
// 传递变更 属性结构
workInProgress,
Component,
// component 层级的变更只受 props 和 context 影响
nextProps: any,
// 渲染优先级
renderLanes
) {
// 开始获取context
let context;
let nextChildren;
prepareToReadContext(workInProgress, renderLanes);
// 省略 dev 环境逻辑 ...
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes
);

if (current !== null && !didReceiveUpdate) {
// 在有当前状态的情况下的情况下 清除hooks
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 其它情况下,进入协调
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}

而这个 renderWithHooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;

// didScheduleRenderPhaseUpdate = false;

// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)

// Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so memoizedState would be null during updates and mounts.
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

let children = Component(props, secondArg);

// Check if there was a render phase update
if (didScheduleRenderPhaseUpdateDuringThisPass) {
// Keep rendering in a loop for as long as render phase updates continue to
// be scheduled. Use a counter to prevent infinite loops.
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);

numberOfReRenders += 1;
// Start over from the beginning of the list
currentHook = null;
workInProgressHook = null;

workInProgress.updateQueue = null;

ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnRerenderInDEV
: HooksDispatcherOnRerender;

children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;

// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;

renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);

currentHook = null;
workInProgressHook = null;

didScheduleRenderPhaseUpdate = false;
return children;
}
有些小细节,比如 通过 memorized 来区分 mount 和 update,这种技巧应用开发中也可以用,比如 useState 的 state 是否是初始值来确定是否是 onMount,这样就可以不用具体生命周期,只用 useEffect

我们看上面的逻辑 ——

协调结构由 didiScheduleRenderPhaseUpdateDuringThisPass 控制!

我们再看 update 中 修改 didiScheduleRenderPhaseUpdateDuringThisPass 的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
}

看到没有?

如果 eagerState 和 currentState 相同,那么直接 return 这个 update 循环

这个 reducer ,说一下, setState(res=>{...res}) 也是dispatch reducer
这里的 state 和 我们写的 state 不同,这里的 state 指的 useState 简单 数据结构 的第一个参数

啥意思呢?如果是 function 组件,是需要对你的 每一个 hooks 进行依赖收集的,如果相关依赖没有改变,这个更新过程,是会被跳过的!

即便你运行了一遍客户端的代码,进入了 updateFunctionComponent ,只要 eagerState 与 currentState 一致,你就不会进入更进一步的 reconciliation (加入 update 队列)

也就是说 ——

ShouldUpdate 在 React function component 中,由 hooks (与 props) 依赖决定!!!

再且问,useContext 是不是 hooks 依赖,返回值有没有被加入 hooks 依赖数组?

那么请问,什么决定了 React function 的 update,是 function 的重新执行么?

不要受 React class 的荼毒太深了!

依赖数组和 props 是唯二影响 update 和渲染的因素!


updateClassComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes
) {
// 获取 context
let hasContext = false;
prepareToReadContext(workInProgress, renderLanes);

const instance = workInProgress.stateNode;
// 注意这里!!!
// 这里出现了 should update !!!
let shouldUpdate;
if (instance === null) {
if (current !== null) {
// A class component without an instance only mounts if it suspended
// inside a non-concurrent tree, in an inconsistent state. We want to
// treat it like a new mount, even though an empty version of it already
// committed. Disconnect the alternate pointers.
current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.flags |= Placement;
}
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, Component, nextProps);
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderLanes
);
} else {
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes
);
}
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes
);
return nextUnitOfWork;
}

后面的 finishClassComponent 我也不贴了,大家可以去看,反正会有一系列的判断,然后再进入 children 的 reconcile

啥意思?

先不管 workInProgress 的静态化,异步过程,调度过程

请问 ——

shouldUpdate 的概念,在 React function component 中,有没有意义?

没有!没有意义
这不是你该纠结的概念!

你可以说我用 React.Memo 包裹防止 props 深层比对(请优先用 hooks 传递数据!),也可以说我 memo 一下,防止出现新的 eagerState !

但是你绝对不能说用 context ,会引发重新渲染!

  • 那是你在 class api 下的使用体验
  • 那是没有依赖收集的使用体验
  • 那是你控制不了调度的使用体验
  • 那是没有 IOC,写代码只考虑数据的使用体验

请注意重新执行 function(调用 updateFunctionComponent 函数),和重新进行 update,reconciliation 的区别!

我用大字写清楚:

  • 重新执行 updateFunctionComponent 函数,是返回 renderWithHooks 的 children,而在 hooks 的逻辑中,如果 eagerState 没有改变,则不会增加 新的 update 到 updateQueue
  • 重新执行函数,依赖未改变,不进行 update
  • 重新执行函数,依赖改变,进行 update
  • 控制你是否 Update 的,是你的依赖!是你的依赖!是你的依赖!

这点无论 Angular,Vue,React 都是如此,没有哪个框架不是如此,老版本不要拿来说了,不是这个时代的东西

是的,你可以说,这里 React 多产生了一个 WorkInProgress,但是它没有新的 update,所以可以理解

当然,你也可以说,ng 直接进行脏检查,而 React 这里实际上也在进行脏检查,还增加了很多内存,拖拽了一个 复杂 Fiber WorkInProgress 结构

是的,这点大家都能看出来,ng 的这部分性能是比 React 强不止一星半点!

这也是为啥 ng 调度可改,React 只能用 RequestAnimationFrame 的原因,确实消耗更大,这个没人不承认

注意,脏检查,也就是变更检测,无论什么 MVVM,MPV 框架都绕不过去,没有任何一个框架可以
React 老版本 批评 AngularJS 的无调度脏检查是另一回事,大家不要拿着个大概映像就开腔

但是,拖拽着这个结构,它能 ——

直接用函数实现 组件 和 服务 啊!!!

最后,再说一下,由于 eagerState 和 currentState 的对比发生 update

组件层级以上

坚持不变性就是在杀死应用性能

你可以 reducer 把变更缩小

但是,正常思维,永远是 useMemo 和 更细化 的 useState

大对象聚焦?useMemo 做 getter,useCalback 做 setter,把对象一部分摘出来就好!

至于用 Redux 那种自带 大对象的

你这是在制造新问题,而且以前你可以解决,现在你根本解决不了

所以,请放心大胆地使用 React DDD!