React 核心源码解析

如果你问一个 React 面试题:”setState 之后发生了什么?”,标准答案应该是——React 创建 update 对象、调度更新、经过 reconciler 的 diff 过程、最后 commit 到 DOM。但如果追问:”reconciler 是怎么遍历 Fiber 树的?diff 算法的 O(n) 是怎么做到的?”,能回答清楚的人就少很多了。

这篇文章从 React 源码的调用链路出发,追溯从 ReactDOM.render 到真实 DOM 更新的完整过程。重点放在两个核心机制上:Fiber 架构为什么要替代 Stack Reconciler,以及 diff 算法做了哪些假设把复杂度从 O(n³) 降到 O(n)。

虚拟 DOM:不一定更快,但一定更可控

虚拟 DOM:不一定更快,但一定更可控

“虚拟 DOM 一定快吗?”——不一定。如果你直接 innerHTML 替换整个容器,虚拟 DOM 远比它快;但如果你精心手写了 appendChildtextContent 的最小更新,虚拟 DOM 的 diff 过程反而是额外开销。

虚拟 DOM 的价值不是”最快”,而是保证性能的下限——不管你写的更新逻辑有多粗糙,React 通过 diff 帮你找到最小变更集。同时它作为中间层,把开发者写的 JSX 翻译为平台无关的 VDOM 树,再由 Reconciler 转化为 Fiber 对象,最终由 Renderer 操作真实 DOM。

源码调用链路:从 render 到 commit

下面是 React 17 的调用链路(React 18 的 Concurrent Mode 增加了调度分支,但核心路径一致)。

  • render
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// react-dom/src/client/ReactDOMLegacy.js 
export function render(
element: React$Element<any>,
container: Container,
callback: ?Function,
) {
// ...
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
  • legacyRenderSubtreeIntoContainer
    创建 fiberroot 的根节点
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
// react-dom/src/client/ReactDOMLegacy.js 
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function,
) {

const maybeRoot = container._reactRootContainer;
let root: FiberRoot;
if (!maybeRoot) {
// Initial mount
root = legacyCreateRootFromDOMContainer(
container,
children,
parentComponent,
callback,
forceHydrate,
);
} else {
root = maybeRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(root);
originalCallback.call(instance);
};
}
// Update
updateContainer(children, root, parentComponent, callback);
}
return getPublicRootInstance(root);
}
  • updateContainer
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
// react-reconciler/src/ReactFiberReconciler.old.js
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// ...
const current = container.current;
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);

if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}

const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}

// ...

const update = createUpdate(eventTime, lane);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};

callback = callback === undefined ? null : callback;
if (callback !== null) {
// ...
update.callback = callback;
}

enqueueUpdate(current, update, lane);
const root = scheduleUpdateOnFiber(current, lane, eventTime);
if (root !== null) {
entangleTransitions(root, current, lane);
}

return lane;
}
  • scheduleUpdateOnFiber
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// react-reconciler/src/ReactFiberWorkLoop.old.js
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
): FiberRoot | null {
checkForNestedUpdates();

const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}

// Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime);

if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
// This update was dispatched during the render phase. This is a mistake
// if the update originates from user space (with the exception of local
// hook updates, which are handled differently and don't reach this
// function), but there are some internal React features that use this as
// an implementation detail, like selective hydration.
warnAboutRenderPhaseUpdatesInDEV(fiber);

// Track lanes that were updated during the render phase
workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
workInProgressRootRenderPhaseUpdatedLanes,
lane,
);
} else {
// This is a normal update, scheduled from outside the render phase. For
// example, during an input event.
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}

warnIfUpdatesNotWrappedWithActDEV(fiber);

if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
if (
(executionContext & CommitContext) !== NoContext &&
root === rootCommittingMutationOrLayoutEffects
) {
if (fiber.mode & ProfileMode) {
let current = fiber;
while (current !== null) {
if (current.tag === Profiler) {
const {id, onNestedUpdateScheduled} = current.memoizedProps;
if (typeof onNestedUpdateScheduled === 'function') {
onNestedUpdateScheduled(id);
}
}
current = current.return;
}
}
}
}

if (enableTransitionTracing) {
const transition = ReactCurrentBatchConfig.transition;
if (transition !== null) {
if (transition.startTime === -1) {
transition.startTime = now();
}

addTransitionToLanesMap(root, transition, lane);
}
}

if (root === workInProgressRoot) {
// TODO: Consolidate with `isInterleavedUpdate` check

// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root. Unless the
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
// phase update. In that case, we don't treat render phase updates as if
// they were interleaved, for backwards compat reasons.
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
) {
workInProgressRootInterleavedUpdatedLanes = mergeLanes(
workInProgressRootInterleavedUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(root, workInProgressRootRenderLanes);
}
}

ensureRootIsScheduled(root, eventTime);
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
return root;
}
  • ensureRootIsScheduled
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// react-reconciler/src/ReactFiberWorkLoop.old.js
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;

// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
markStarvedLanesAsExpired(root, currentTime);

// Determine the next lanes to work on, and their priority.
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);

if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}

// We use the highest priority lane to represent the priority of the callback.
const newCallbackPriority = getHighestPriorityLane(nextLanes);

// Check if there's an existing task. We may be able to reuse it.
const existingCallbackPriority = root.callbackPriority;
if (
existingCallbackPriority === newCallbackPriority &&
// Special case related to `act`. If the currently scheduled task is a
// Scheduler task, rather than an `act` task, cancel it and re-scheduled
// on the `act` queue.
!(
__DEV__ &&
ReactCurrentActQueue.current !== null &&
existingCallbackNode !== fakeActCallbackNode
)
) {
// ...
// The priority hasn't changed. We can reuse the existing task. Exit.
return;
}

if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}

// Schedule a new callback.
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
ReactCurrentActQueue.didScheduleLegacyUpdate = true;
}
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
if (supportsMicrotasks) {
// Flush the queue in a microtask.
if (__DEV__ && ReactCurrentActQueue.current !== null) {
// Inside `act`, use our internal `act` queue so that these get flushed
// at the end of the current scope even when using the sync version
// of `act`.
ReactCurrentActQueue.current.push(flushSyncCallbacks);
} else {
scheduleMicrotask(() => {
// In Safari, appending an iframe forces microtasks to run.
// https://github.com/facebook/react/issues/22459
// We don't support running callbacks in the middle of render
// or commit so we need to check against that.
if (executionContext === NoContext) {
// It's only safe to do this conditionally because we always
// check for pending work before we exit the task.
flushSyncCallbacks();
}
});
}
} else {
// Flush the queue in an Immediate task.
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
newCallbackNode = null;
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}

root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
  • performSyncWorkOnRoot
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
// react-reconciler/src/ReactFiberWorkLoop.old.js
function performSyncWorkOnRoot(root) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
syncNestedUpdateFlag();
}

if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}

flushPassiveEffects();

let lanes = getNextLanes(root, NoLanes);
if (!includesSomeLane(lanes, SyncLane)) {
// There's no remaining sync work left.
ensureRootIsScheduled(root, now());
return null;
}

let exitStatus = renderRootSync(root, lanes);
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
// If something threw an error, try rendering one more time. We'll render
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
}
}

if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root, now());
throw fatalError;
}

if (exitStatus === RootDidNotComplete) {
throw new Error('Root did not complete. This is a bug in React.');
}

// We now have a consistent tree. Because this is a sync render, we
// will commit it even if something suspended.
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root, workInProgressRootRecoverableErrors);

// Before exiting, make sure there's a callback scheduled for the next
// pending level.
ensureRootIsScheduled(root, now());

return null;
}
  • renderRootSync
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
// react-reconciler/src/ReactFiberWorkLoop.old.js
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();

// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}

// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}

workInProgressTransitions = getTransitionsForLanes(root, lanes);
prepareFreshStack(root, lanes);
}

// ...

if (enableSchedulingProfiler) {
markRenderStarted(lanes);
}

do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();

executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);

if (workInProgress !== null) {
// This is a sync render, so we should have finished the whole tree.
throw new Error(
'Cannot commit an incomplete root. This error is likely caused by a ' +
'bug in React. Please file an issue.',
);
}

// ...

if (enableSchedulingProfiler) {
markRenderStopped();
}

// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;

return workInProgressRootExitStatus;
}

  • workLoopSync
1
2
3
4
5
6
7
// react-reconciler/src/ReactFiberWorkLoop.old.js
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
  • performUnitOfWork
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
// react-reconciler/src/ReactFiberWorkLoop.old.js
function performUnitOfWork(unitOfWork: Fiber): void {
// 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.
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);

let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}

resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}

ReactCurrentOwner.current = null;
}
  • beginWork
1
// react-reconciler/src/ReactFiberBeginWork.old.js
  • completeWork
1
// react-reconciler/src/ReactFiberCompleteWork.old.js

完整链路总结

ReactDOM.renderlegacyRenderSubtreeIntoContainer(创建 FiberRoot)→ updateContainer(创建 Update 并入队)→ scheduleUpdateOnFiber(调度)→ ensureRootIsScheduled(分同步/并发两条路)→ performSyncWorkOnRoot(同步)或 performConcurrentWorkOnRoot(并发)→ renderRootSyncworkLoopSyncperformUnitOfWork(循环处理每个 Fiber 节点)→ beginWork(向下调和)→ completeWork(向上归并,创建 DOM)→ commitRoot(提交到真实 DOM)

diff 算法:三个假设换来的 O(n)

React 如何把 diff 复杂度降下来?

Tree diff 的最优算法复杂度是 O(n³),对 1000 个节点做一次 diff 需要十亿次比较,不可用。React 做了三个启发式假设:

  1. 只对同级元素进行 diff:如果节点跨层级移动,直接删除重建,不做跨层级比较
  2. 类型不同的节点直接替换<div><span>,整棵子树删除重建
  3. 通过 key 标识同级子元素:同层级子元素通过 key 缓存实例,尽量复用而非重建

这三个假设把复杂度从 O(n³) 降到了 O(n)——好情况是 O(n),最差情况 O(mn)。

Fiber:为什么需要可中断的渲染

在 React 16 之前,协调器是 Stack Reconciler——递归遍历 Virtual DOM 树,整个过程是同步不可中断的。对于庞大的 DOM 树,一次 reconciliation 可能耗时上百毫秒,在这期间主线程被 JS 占用,任何交互、布局、渲染都会停止。

Fiber Reconciler 的核心改变:任务可中断、可恢复、有优先级。React 把 reconciliation 分解为一个个 Fiber 节点的工作单元,每次处理完一个单元后检查是否还有剩余时间,如果没有就交出主线程给浏览器处理用户交互。

实现上,React 使用 MessageChannel 模拟 requestIdleCallback 的行为(不用 requestIdleCallback 本身的原因:兼容性不够好,且 Chrome 的 50ms 调度间隔对 UI 渲染不够精细)。也没有用 Generator,因为 Generator 的 yield 会丢失调用栈。

调度机制

在 Concurrent Mode(React 18 默认开启)中,scheduler 包负责管理任务优先级。耗时任务被放入队列,以一定的节奏执行——高优先级的用户交互可以打断低优先级的渲染,渲染可以恢复。

使用:

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
import { render } from "./render";
import { createElement } from "./react";

// 开发:

const vnode = createElement(
"ul",
{
id: "ul-test",
className: "padding-20",
style: {
padding: "10px",
},
},
createElement("li", { key: "li-0" }, "this is li 01")
);

const nextVNode = createElement(
"ul",
{
style: {
width: "100px",
height: "100px",
backgroundColor: "green",
},
},
[
createElement("li", { key: "li-a" }, "this is li a"),
createElement("li", { key: "li-b" }, "this is li b"),
createElement("li", { key: "li-c" }, "this is li c"),
createElement("li", { key: "li-d" }, "this is li d"),
]
);

const lastVNode = createElement(
"ul",
{
style: {
width: "100px",
height: "200px",
backgroundColor: "pink",
},
},
[
createElement("li", { key: "li-a" }, "this is li a"),
createElement("li", { key: "li-c" }, "this is li c"),
createElement("li", { key: "li-d" }, "this is li d"),
createElement("li", { key: "li-f" }, "this is li f"),
createElement("li", { key: "li-b" }, "this is li b"),
]
);

setTimeout(() => render(vnode, document.getElementById("app")))
setTimeout(() => render(nextVNode, document.getElementById("app")),6000)
setTimeout(() => render(lastVNode, document.getElementById("app")),8000)
console.log(nextVNode);

code:

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

const normalize = (children = []) => children.map(child => typeof child === 'string' ? createVText(child): child)

export const NODE_FLAG = {
EL: 1, // 元素 element
TEXT: 1 << 1
};
// El & TEXT = 0


const createVText = (text) => {
return {
type: "",
props: {
nodeValue: text + ""
},
$$: { flag: NODE_FLAG.TEXT }
}
}

const createVNode = (type, props, key, $$) => {
return {
type,
props,
key,
$$,
}
}

export const createElement = (type, props, ...kids) => {
props = props || {};
let key = props.key || void 0;
kids = normalize(props.children || kids);

if(kids.length) props.children = kids.length === 1? kids[0] : kids;

// 定义一下内部的属性
const $$ = {};
$$.staticNode = null;
$$.flag = type === "" ? NODE_FLAG.TEXT: NODE_FLAG.EL;

return createVNode(type, props, key, $$)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { mount } from "./mount";
import { patch } from "./patch";

// step 1
// setTimeout(() => render(vnode, document.getElementById("app")))

// step 2
// setTimeout(() => render(null, document.getElementById("app")),5000)

export function render(vnode, parent) {
let prev = parent.__vnode;
if(!prev) {
mount(vnode, parent);
parent.__vnode = vnode;
} else {
if(vnode) {
// 新旧两个
patch(prev, vnode, parent);
parent.__vnode = vnode;
} else {
parent.removeChild(prev.staticNode)
}
}
}
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
import { patchProps } from "./patch";
import { NODE_FLAG } from "./react";

export function mount(vnode, parent, refNode) {
// 为什么会有一个 refNode?
/**
* 假如: ul -> li li li(refNode)
*/
if(!parent) throw new Error('no container');
const $$ = vnode.$$;

if($$.flag & NODE_FLAG.TEXT) {
// 如果是一个文本节点
const el = document.createTextNode(vnode.props.nodeValue);
vnode.staticNode = el;
parent.appendChild(el);
} else if($$.flag & NODE_FLAG.EL) {
// 如果是一个元素节点的情况,先不考虑是一个组件的情况;
const { type, props } = vnode;
const staticNode = document.createElement(type);
vnode.staticNode = staticNode;

// 我们再来处理,children 和后面的内容
const { children, ...rest} = props;
if(Object.keys(rest).length) {
for(let key of Object.keys(rest)) {
// 属性对比的函数
patchProps(key, null, rest[key], staticNode);
}
}

if(children) {
// 递归处理子节点
const __children = Array.isArray(children) ? children : [children];
for(let child of __children) {
mount(child, staticNode);
}
}
refNode ? parent.insertBefore(staticNode, refNode) : parent.appendChild(staticNode);
}

}
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { mount } from "./mount";
import { diff } from './diff';

function patchChildren(prev, next, parent) {
// diff 整个的逻辑还是耗性能的,所以,我们可以先提前做一些处理。
if(!prev) {
if(!next) {
// nothing
} else {
next = Array.isArray(next) ? next : [next];
for(const c of next) {
mount(c, parent);
}
}
} else if (prev && !Array.isArray(prev)) {
// 只有一个 children
if(!next) parent.removeChild(prev.staticNode);
else if(next && !Array.isArray(next)) {
patch(prev, next, parent)
} else {
// 如果prev 只有一个节点,next 有多个节点
parent.removeChild(prev.staticNode);
for(const c of next) {
mount(c, parent);
}
}
} else diff(prev, next, parent);
}

export function patch (prev, next, parent) {
// type: 'div' -> 'ul'
if(prev.type !== next.type) {
parent.removeChild(prev.staticNode);
mount(next, parent);
return;
}

// type 一样,diff props
// 先不看 children
const { props: { children: prevChildren, ...prevProps}} = prev;
const { props: { children: nextChildren, ...nextProps}} = next;
// patch Porps
const staticNode = (next.staticNode = prev.staticNode);
for(let key of Object.keys(nextProps)) {
let prev = prevProps[key],
next = nextProps[key]
patchProps(key, prev, next, staticNode)
}

for(let key of Object.keys(prevProps)) {
if(!nextProps.hasOwnProperty(key)) patchProps(key, prevProps[key], null, staticNode);
}

// patch Children !!!
patchChildren(
prevChildren,
nextChildren,
staticNode
)

}


export function patchProps(key, prev, next, staticNode) {
// style
if(key === "style") {
// margin: 0 padding: 10
if(next) {
for(let k in next) {
staticNode.style[k] = next[k];
}
}
if(prev) {
// margin: 10; color: red
for(let k in prev) {
if(!next.hasOwnProperty(k)) {
// style 的属性,如果新的没有,老的有,那么老的要删掉。
staticNode.style[k] = "";
}
}
}
}

else if(key === "className") {
if(!staticNode.classList.contains(next)) {
staticNode.classList.add(next);
}
}

// events
else if(key[0] === "o" && key[1] === 'n') {
prev && staticNode.removeEventListener(key.slice(2).toLowerCase(), prev);
next && staticNode.addEventListener(key.slice(2).toLowerCase(), next);

} else if (/\[A-Z]|^(?:value|checked|selected|muted)$/.test(key)) {
staticNode[key] = next

} else {
staticNode.setAttribute && staticNode.setAttribute(key, next);
}
}
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
import { mount } from './mount.js'
import { patch } from './patch.js'

export const diff = (prev, next, parent) => {
let prevMap = {}
let nextMap = {}

// 遍历我的老的 children
for (let i = 0; i < prev.length; i++) {
let { key = i + '' } = prev[i]
prevMap[key] = i
}

let lastIndex = 0
// 遍历我的新的 children
for (let n = 0; n < next.length; n++) {
let { key = n + '' } = next[n]
// 老的节点
let j = prevMap[key]
// 新的 child
let nextChild = next[n]
nextMap[key] = n
// 老的children 新的children
// [b, a] [c, d, a] => [c, b, a] --> c
// [b, a] [c, d, a] => [c, d, b, a] --> d

if (j == null) {
// 从老的里面,没有找到。新插入
let refNode = n === 0 ? prev[0].staticNode : next[n - 1].staticNode.nextSibling
mount(nextChild, parent, refNode)
}
else {
// [b, a] [c, d, a] => [c, d, a, b] --> a
// 如果找到了,我 patch
patch(prev[j], nextChild, parent)

if (j < lastIndex) {
// 上一个节点的下一个节点的前面,执行插入
let refNode = next[n - 1].staticNode.nextSibling;
parent.insertBefore(nextChild.staticNode, refNode)
}
else {
lastIndex = j
}
}
}
// [b, a] [c, d, a] => [c, d, a] --> b
for (let i = 0; i < prev.length; i++) {
let { key = '' + i } = prev[i]
if (!nextMap.hasOwnProperty(key)) parent.removeChild(prev[i].staticNode)
}
}