React 核心源码解析

课程目标

  1. 知道 react ⼤致实现思路,能对⽐ react 和 js 控制原⽣ dom 的差异,能口述⼀个简化版的 react。
  2. 知道 diff 算法⼤致实现思路。
  3. 对 state 和 props 有⾃⼰的使⽤⼼得,结合受控组件、HOC等特性描述,需要说明各种⽅案的适⽤场景。
  4. 能说明⽩为什么要实现 fiber。
  5. 能说明⽩为什么要实现 hook。
  6. 知道react不常⽤的特性,⽐如context,portal,Error boundaries。

知识要点

虚拟 DOM

不管是 React 还是 Vue,都有虚拟 DOM ,虚拟 DOM 一定快吗?
为什么要用虚拟 DOM?

  • 某种程度上,保证,性能的一个下限;
  • 中间层,VDOM -> fiber 对象 -> 真实 DOM

源码跟踪

  • 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

diff 算法

react 是如何将 diff 算法的复杂度降下来的?

其实就是在算法复杂度、虚拟 dom 渲染机制、性能中找了⼀个平衡,react 采⽤了启发式的算法,做了如下最优假设:

  1. 如果节点类型相同,那么以该节点为根节点的 tree 结构,⼤概率是相同的,所以如果类型不同,可以直接「删除」原节点,「插⼊」新节点
  2. 跨层级移动⼦ tree 结构的情况⽐较少⻅,或者可以培养⽤户使⽤习惯来规避这种情况,遇到这种情况同样是采⽤先「删除」再「插⼊」的⽅式,这样就避免了跨层级移动
  3. 同⼀层级的⼦元素,可以通过 key 来缓存实例,然后根据算法采取「插⼊」「删除」「移动」的操作,尽量复⽤,减少性能开销
  4. 完全相同的节点,其虚拟 dom 也是完全⼀致的;

调度

concurrent 模式 / 18⾥⾯ – 调度。

scheduler – 有⼀个任务,耗时很⻓,filter

--> 把任务,放进⼀个队列,然后开始以⼀种节奏进⾏执⾏。

使用 MessageChannel 模拟 requestIdleCallback

requestIdleCallback.jsview raw
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
/**
* schedule —> 把任务放进一个队列里,然后以某一种节奏进行执行;
*/

// task 的任务队列
const queue = [];
const threshold = 1000 / 60;

const transitions = [];
let deadline = 0;

// 获取当前时间, bi date-now 精确
const now = () => performance.now(); // 时间 ,精确
// 从任务queue中,选择第一个 任务
const peek = arr => arr.length === 0 ? null : arr[0];

// schedule —> 把我的任务放进一个队列里,然后以某一种节奏进行执行;
export function schedule (cb) {
queue.push(cb);
startTransitions(flush);
}

// 此时,是否应该交出执行权
function shouldYield() {
return navigator.scheduling.isInputPending() || now() >= deadline;
}

// 执行权的切换
function startTransitions(cb) {
transitions.push(cb) && postMessage();
}

// 执行权的切换
const postMessage = (() => {
const cb = () => transitions.splice(0, 1).forEach(c => c());
const { port1, port2 } = new MessageChannel();
port1.onmessage = cb;
return () => port2.postMessage(null);
})()

// 模拟实现 requestIdleCallback 方法
function flush() {
// 生成时间,用于判断
deadline = now() + threshold;
let task = peek(queue);

// 还没有超出 16.666ms 同时,也没有更高的优先级打断我
while(task && !shouldYield()) {
const { cb } = task;
const next = cb();
// 相当于有一个约定,如果,这个 task 返回的是一个函数,那下一次,就从这里接着跑
// 那如果 task 返回的不是函数,说明已经跑完了。不需要再从这里跑了
if(next && typeof next === "function") {
task.cb = next;
} else {
queue.shift()
}
task = peek(queue);
}

// 如果这一个时间片执行完了
task && startTransitions(flush)
}

为什么没有用 requestIdleCallback

  • 兼容性;
  • 50ms 渲染问题;
    chrome 60hz 每16.666ms 执⾏⼀次事件循环。

为什么没有⽤ generator ?
why not use generator

为什么没有⽤ setTimeout ? – 4-5ms

补充知识点

react 和 Vue 的 diff 算法有什么区别?

  • 相同点:
    Vue 和react 的 diff 算法,都是不进⾏跨层级⽐较,只做同级⽐较。

  • 不同点:

    1. Vue 进⾏ diff 时,调⽤ patch 打补丁函数,⼀边⽐较⼀边给真实的DOM打补丁
    2. Vue 对⽐节点,当节点元素类型相同,但是 className 不同时,认为是不同类型的元素,删除重新创建,⽽ react 则认为是同类型节点,进⾏修改操作
      1. Vue 的列表⽐对,采⽤从两端到中间的⽅式,旧集合和新集合两端各存在两个指针,两两进⾏⽐较,如果匹配上了就按照新集合去调整旧集合,每次对⽐结束后,指针向队列中间移动;
      2. ⽽ react 则是从左往右依次对⽐,利⽤元素的 index 和标识 lastIndex 进⾏⽐较,如果满⾜ index < lastIndex 就移动元素,删除和添加则各⾃按照规则调整;
      3. 当⼀个集合把最后⼀个节点移动到最前⾯,react 会把前⾯的节点依次向后移动,⽽ Vue 只会把最后⼀个节点放在最前⾯,这样的操作来看,Vue 的 diff 性能是⾼于 react 的

⽬前的 diff 是什么?真的是 O(n) 吗?

准确的来说,React的 diff 的最好时间复杂度是 O(n), 最差的话,是 O(mn);

diff ⽐较的是什么?

比较的是 current fiber 和 vdom,比较之后生成 workingprogress fiber

为什么要有 key ?

在⽐较时,会以 key 和 type 是否相同进⾏⽐较,如果相同,则直接复制。

react 为什么要⽤ fiber ?

stack reconciler -> fiber reconciler

在 V16 版本之前协调机制是 Stack reconciler, V16 版本发布 Fiber 架构后是 Fiber reconciler。

在 setState 后,react 会⽴即开始 reconciliation 过程,从⽗节点(Virtual DOM)开始递归遍历,以找出不同。将所有的 Virtual DOM 遍历完成后,reconciler 才能给出当前需要修改真实 DOM 的信息,并传递给 renderer,进⾏渲染,然后屏幕上才会显示此次更新内容。

对于特别庞⼤的DOM树来说,reconciliation 过程会很⻓(x00ms),在这期间,主线程是被 js 占⽤的,因此任何交互、布局、渲染都会停⽌,给⽤户的感觉就是⻚⾯被卡住了。

在这⾥我们想解决这个问题的话,来引⼊⼀个概念,就是任务可中断,以及任务优先级,也就是说我们的 reconciliation 的过程中会⽣成⼀些任务和⼦任务,⽤户的操作的任务优先级是要⾼于 reconciliation 产⽣的任务的,也就是说⽤户操作的任务是可以打断 reconciliation 中产⽣的任务的,它会优先执⾏。

react 为什么要⽤ hook ?

React Doc

  • 在组件之间复用状态逻辑很难
    Hook 使你在无需修改组件结构的情况下复用状态逻辑。
  • 复杂组件变得难以理解
    Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
  • 难以理解的 class
    Hook 使你在非 class 的情况下可以使用更多的 React 特性

hook 是怎么玩的 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
function App () {
const [state, setState] = useState(0)
const divRef = useRef(null)

return <div>XXX<div>
}
// App 也是一个 Fiber
// 在 beginWork 的时候 App() -> vdom
// AppFiber.memoizedState -> hook.next -> hook.next - hook
// hook.memoizedState - 保存了 hook 对应的属性



类 React 最小实现

使用:

index.jsview raw
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:

react.jsview raw
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
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, $$)
}
render.jsview raw
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)
}
}
}
mount.jsview raw
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);
}

}
patch.jsview raw
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);
}
}
diff.jsview raw
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)
}
}