4-5 React 源码解析

164 阅读12分钟

原文链接(格式更好):《4-5 React 源码解析》

官方源码:github.com/facebook/re…

预防针:看过 Vue 源码的正常,不看 React 源码的也正常,因为它非常复杂,未必能看懂,所以看懂一部分就行了。

看了 React 源码与写好 React 没啥关系。

这篇文章能带来的是:搞清楚 React 的工作流程,基于 React 的 17 版本源码

引子

<div>
	<h2> hello world </h2>
  <div>{ text }</div>
  {
    list.map(item => <ChildItem item={item}></ChildItem>)
  }
</div>

问题:list 数据改变时,怎么最快的判断出来如何更新?

答案:React 为了保持运行时的灵活性,一律采用从头(根节点)遍历,然后再跟之前的对比,把有区别的更新下

疑问:那若有很多 HTML,React 就比较耗性能了吧?是的!

方案:后续 React 的版本就一直在解决这个问题。比如采用了 fiber、使用异步可中断更新策略等等。

React 为什么要用 Fiber?

因为 React 采用的更新策略是从根节点递归遍历,然后再跟之前的对比,把有区别的更新下

然后在 15 版本,采用的是 Stack Reconciler 同步更新机制,由于是同步的,则会存在阻塞,并且还不支持中断

在之后的版本,采用了 Fiber Reconciler 更新机制,支持异步可中断的遍历/更新,若有优先级高的交互(输入、点击等),则中断遍历/更新,先去处理交互,再接着遍历/更新

React 大概的版本区别

  • V15 版本,Stack Reconciler:同步更新机制
  • 16.9 ~ 17.0.2 版本,Fiber Reconciler:异步可中断更新机制
    • 但在 17.0.2 里面只是先做了数据结构,但不稳定(可以理解为先吹了一波),有两个模式
      • legacy 模式:可通过 createa 脚手架创建,有 Fiber 的结构,但不会中断,源码文件为 xxx.old.js
      • concurrent 模式:需要自己去编译,无法通过 createa 脚手架创建,实现了中断,源码文件为 xxx.new.js
  • 18 版本,可以理解为 concurrent 模式 ++

React 中的数据结构

v-dom / element

function App() {
  return <div className="app">
    <h2>hello world</h2>
    <div id="list">
      <ul>
        <li>list 1</li>
        <li>list 2</li>
        <li>list 3</li>
      </ul>
    </div>
  </div>
}

// 经过 babel 编译后为:
function App() {
  return React.createElement('div', { className: 'app'}, 
    React.createElement('h2', null, 'hello world'),
    React.createElement('div', { id: 'list' },
      React.createElement('ul', null, 
        React.createElement('li', null, 'list 1'),
        React.createElement('li', null, 'list 2'),
        React.createElement('li', null, 'list 3')
      )
    )
  )
}

React.createElement到底是个啥?源码地址:点这个

核心代码:

function ReactElement(type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  // ...

  return element;
}


export function createElement(type, config, children) {
  let propName;

  const props = {};

  // ⭐️ 将 config 的值放入 props 中
  if (config != null) {
    // ...
    
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // ⭐️ 将 children 的值放入 props.children 中
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }
  
	// ...
  
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

通过上述的React.createElement源码可得如下结构(vDom):

function App() {
  return React.createElement('div', { className: 'app'}, 
    React.createElement('h2', null, 'hello world'),
    React.createElement('div', { id: 'list' },
      React.createElement('ul', null, 
        React.createElement('li', null, 'list 1'),
        React.createElement('li', null, 'list 2'),
        React.createElement('li', null, 'list 3')
      )
    )
  )
}

// 得出如下结构(简版)
const vDom = {
  type: "div",
  props: {
    className: "app",
    children: [
      {
        type: "h2",
        props: { children: "hello world" },
      },
      {
        type: "div",
        props: {
          id: "list",
          children: [
            {
              type: "ul",
              props: {
                children: [
                  {
                    type: "li",
                    props: { children: "list 1" },
                  },
                  {
                    type: "li",
                    props: { children: "list 2" },
                  },
                  {
                    type: "li",
                    props: { children: "list 3" },
                  },
                ],
              },
            },
          ],
        },
      },
    ],
  },
};

fiber

本质就是一个链表形式的数据结构,用来表示 v-dom 的,大致结构如下:

FiberNode = {
  tag, // 标明 fiber 的类型,如函数组件、类组件等
  key, // React 元素的key,用于优化更新
  elementType, // 标明 React 元素的类型,即调用 createElement 时的第一个参数。
  type, // 标明 DOM 元素 的类型

  // 链表形式
  return, // 指向父 fiber
  child, // 指向子 fiber
  sibling // 指向下一个同级 fiber
}

current Fiber、workInProgress Fiber

current Fiber:对应当前页面中真实渲染的 DOM 的结构

workInProgress Fiber:更新时才创建的并已处理好变更的对应到真实 DOM 的虚拟 DOM 的 Fiber 链表

大致的链表结构如下:

React 的双缓存是什么?

双缓存来源于:图形渲染的优化技术

原理为:

  1. 在双缓存技术中,系统会在内存(通常为显存)中开辟两个区域:
    • 前缓冲(Front Buffer):这是与显示器相连的缓冲区,当前正在被显示的内容就存储在这里。
    • 后缓冲(Back Buffer):这是一个独立于前缓冲的内存区域,应用程序在该区域进行所有的绘图操作。
  1. 应用程序会在后缓冲中完成所有的图形绘制工作,而不会直接影响到屏幕上显示的内容。
  2. 当所有需要更新的画面内容都在后缓冲区绘制完成后,系统会执行一次“缓冲区交换”或“页面翻转”操作,迅速将前后缓冲区的角色互换,即将后缓冲区的内容复制到前缓冲区,并使其成为新的可见帧。
  3. 由于这个交换操作通常是硬件加速且非常快速的,因此用户看到的是一个完整、无闪烁的新帧画面,而不是半成品或者绘制过程中的中间状态。

简单理解为:预加载

React 中的双缓存指的是两个 Fiber 树,一个用于当前展示(current fiber),一个用于更新的(workInprogress fiber),有变化时则创建workInprogress fiber并在其中进行比较、计算等,之后直接一次性用整个workInprogress fiber(或部分) 替换掉整个current fiber(或部分),不用两个逐个比较与 DOM 更新,从而避免了大量的 DOM 操作带来的性能损耗。

React 的核心库

  • react 库1、提供虚拟 DOM 相关的 API;2、提供用户相关的 API(useState/use* 等)
  • react-dom 库提供操作 DOM 相关的 API
    • 其中的核心库react-reconciler通过调和、调度、提交等过程实现渲染
      • 其中核心的为:
        • ReactFiberBeginWork.js:创建 workInProgressFiber
        • ReactFiberCompleteWork.js:基于 workInProgressFiber 创建 EffectList
        • ReactFiberCommitWork.js:基于 EffectList 更新界面

React 的整体流程(V17 版本)

1. 页面代码

function App() {
  return <div className="app">
    <h2>hello world</h2>
    <div id="list">
      <ul>
        <li>list 1</li>
        <li>list 2</li>
        <li>list 3</li>
      </ul>
    </div>
  </div>
}

// 入口:ReactDom.render(...)
ReactDom.render(<App />, document.getElementById('root'));

2. 入口:页面的ReactDom.render(...),调用的源码是react-dom 库里面的render函数

// react-dom 源码
function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
){
  // ...
  
  return legacyRenderSubtreeIntoContainer( // 渲染下一级的树到 Container 中
    null,
    element,
    container,
    false,
    callback,
  );
}

3. legacyRenderSubtreeIntoContainer:将子树渲染到容器中,首次渲染则调用legacyCreateRootFromDOMContainer

// 作用:将子树渲染到容器中。
// 具体来说,它是在 React 的旧版本和新的 Fiber 架构之间进行兼容性处理的一部分。
function  legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>, // 父组件(可能为null)
  children: ReactNodeList, // 要渲染的子节点。
  container: Container, // 容器,即 DOM 元素或 React 容器
  forceHydrate: boolean, // 一个布尔值,决定是否强制进行 hydration(将服务器端渲染的HTML转换为客户端React组件)。
  callback: ?Function, // 回调函数,在渲染完成后调用
): React$Component<any, any> | PublicInstance | null {
	// ...

  // 从给定的容器中获取 _reactRootContainer ,这可能是根容器
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) { // 不存在,即容器是首次渲染:
    // ⭐️ Initial mount
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;

    // ...

    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else { // 存在,即容器不是首次渲染
    fiberRoot = root._internalRoot;

    // ...
    
    // Update,更新容器的内容
    updateContainer(children, fiberRoot, parentComponent, callback);
  }

  // 返回根容器的公共实例
  return getPublicRootInstance(root);
}

4. legacyCreateRootFromDOMContainer:用于创建 React 的根节点(FiberRoot),核心为createLegacyRoot

function legacyCreateRootFromDOMContainer(
  container: Container,
  forceHydrate: boolean,
): RootType {
  // ...

  return createLegacyRoot(
    container,
    shouldHydrate
      ? {
          hydrate: true,
        }
      : undefined,
  );
}

5. createLegacyRoot:用于创建 React 的根节点(FiberRoot),核心为ReactDOMBlockingRoot

export function createLegacyRoot(
  container: Container,
  options?: RootOptions,
): RootType {
  return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

6. ReactDOMBlockingRoot:核心调用createRootImpl

function ReactDOMBlockingRoot(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  this._internalRoot = createRootImpl(container, tag, options);
}

ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(){ ... }
ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = function(): void { ... }

7. createRootImpl:核心调用createContainer

function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  // ...
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);

  // ...
  
  return root;
}

8. createContainer:核心调用createFiberRoot

export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}

9. createFiberRoot:核心调用new FiberRootNode

export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);

  const rootFiber = createHostRootFiber(tag);
  root.current = rootFiber; // ⭐️ current Fiber
  // ...

  return root;
}

10. FiberRootNode:一个构造函数,生成Fiber Root Node的数据结构

function FiberRootNode(containerInfo, tag, hydrate) {
  this.tag = tag;
  this.containerInfo = containerInfo;
  this.pendingChildren = null;
  this.current = null;
  this.pingCache = null;
  this.finishedWork = null;
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.hydrate = hydrate;
  this.callbackNode = null;
  this.callbackPriority = NoLanePriority;
  this.eventTimes = createLaneMap(NoLanes);
  this.expirationTimes = createLaneMap(NoTimestamp);

  this.pendingLanes = NoLanes;
  this.suspendedLanes = NoLanes;
  this.pingedLanes = NoLanes;
  this.expiredLanes = NoLanes;
  this.mutableReadLanes = NoLanes;
  this.finishedLanes = NoLanes;

  this.entangledLanes = NoLanes;
  this.entanglements = createLaneMap(NoLanes);

  if (supportsHydration) {
    this.mutableSourceEagerHydrationData = null;
  }

  if (enableSchedulerTracing) {
    this.interactionThreadID = unstable_getThreadID();
    this.memoizedInteractions = new Set();
    this.pendingInteractionMap = new Map();
  }
  if (enableSuspenseCallback) {
    this.hydrationCallbacks = null;
  }
}

11. 总结一下前面创建Fiber Root的流程:

12. 创建之后,后续关键调用为updateContainer,其中关键为scheduleUpdateOnFiber

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  // ...

  const current = container.current;

  // ...

  const update = createUpdate(eventTime, lane);

  // ...
  
  scheduleUpdateOnFiber(current, lane, eventTime);

  return lane;
}

13. scheduleUpdateOnFiber:关键调用performSyncWorkOnRoot

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  // ...

  // 关键函数:1
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  

  // ...

  if (lane === SyncLane) {
    	// ...

    	// 关键函数:2
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);
      if (executionContext === NoContext) {
        // 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();
        flushSyncCallbackQueue();
      }
    }
  } else {
    // Schedule a discrete update but only if it's not Sync.
    if (
      (executionContext & DiscreteEventContext) !== NoContext &&
      // Only updates at user-blocking priority or greater are considered
      // discrete, even inside a discrete event.
      (priorityLevel === UserBlockingSchedulerPriority ||
        priorityLevel === ImmediateSchedulerPriority)
    ) {
      // This is the result of a discrete event. Track the lowest priority
      // discrete update per root so we can flush them early, if needed.
      if (rootsWithPendingDiscreteUpdates === null) {
        rootsWithPendingDiscreteUpdates = new Set([root]);
      } else {
        rootsWithPendingDiscreteUpdates.add(root);
      }
    }
    // Schedule other updates after in case the callback is sync.
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
  }

  // We use this when assigning a lane for a transition inside
  // `requestUpdateLane`. We assume it's the same as the root being updated,
  // since in the common case of a single root app it probably is. If it's not
  // the same root, then it's not a huge deal, we just might batch more stuff
  // together more than necessary.
  mostRecentlyUpdatedRoot = root;
}

14. performSyncWorkOnRoot:关键调用renderRootSynccommitRoot

function performSyncWorkOnRoot(root) {
  // ...
  
  exitStatus = renderRootSync(root, lanes);

  // ...

  commitRoot(root);

  // ...

  return null;
}

15. renderRootSync:关键调用workLoopSync

function renderRootSync(root: FiberRoot, lanes: Lanes) {
  // ...
   
  workLoopSync();
  
  // ...
  
  return workInProgressRootExitStatus;
}

16. workLoopSync:递归调用performUnitOfWork

// 一个递归函数,逻辑为根据最开始的页面代码来调用的
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// 最开始的页面代码
function App() {
  return <div className="app">
    <h2>hello world</h2>
    <div id="list">
      <ul>
        <li>list 1</li>
        <li>list 2</li>
        <li>list 3</li>
      </ul>
    </div>
  </div>
}
// 入口:ReactDom.render(...)
ReactDom.render(<App />, document.getElementById('root'));

则 workLoopSync 调用 performUnitOfWork 时的参数为:
第①次:tag = 3,elementType = null,代表 container
第②次:tag = 2,elementType = f App(),代表 function App() { ...} 函数
第③次:tag = 5,elementType = 'div',代表 <div className="app"> 元素
第④次:tag = 5,elementType = 'h2',代表 <h2> 元素
第⑤次:tag = 5,elementType = 'div',代表 <div id="list"> 元素
...
第n次:tag = 5,elementType = 'li',代表 <li>list 3</li> 元素

17. performUnitOfWork:被递归调用,核心调用beginWorkcompleteUnitOfWork

function performUnitOfWork(unitOfWork: Fiber): void {
  // ...

  let next;

  // ...

  // 调用 beginWork 处理当前元素,返回值为子节点,没有则返回 null
  next = beginWork(current, unitOfWork, subtreeRenderLanes);

  // ...

  if (next === null) {
    // 若无子节点,则完成当前任务
    completeUnitOfWork(unitOfWork);
  } else {
    // 否则继续处理子节点
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

所以递归逻辑为:先子级后同级

workLoopSync递归调用performUnitOfWork处理同级,performUnitOfWork调用beginWork处理子级

function App() {
  return <div className="app">
    <h2>hello world</h2>
    <div id="list">
      <ul>
        <li>list 1</li>
        <li>list 2</li>
        <li>list 3</li>
      </ul>
    </div>
  </div>
}

a. 递归调用逻辑图(Fiber 链表结构)

上图体现的就是Fiber的数据结构,核心就是:return(父)、child(子)、sibling(兄)

18. beginWork:作用是创建 workInProgressFiber

生成v-dom,然后和current fiber对比,向下调和的过程

就是由 fiberrRoot 按照 child 指针逐层往下调和,期间会执行:函数组件、类组件,DIFF 子节点,从而打上不同的effectTag

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const updateLanes = workInProgress.lanes;

	// ...

  if (current !== null) {
  	// 初次渲染不会进
    
    // 新老对比
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
  }

  // 匹配节点的 tag 类型,然后调用对应的方法去
  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes,
      );
    }
    case LazyComponent: {
      const elementType = workInProgress.elementType;
      return mountLazyComponent(
        current,
        workInProgress,
        elementType,
        updateLanes,
        renderLanes,
      );
    }
    // ...
  }
}

19. completeUnitOfWork:作用是根据effectTag,创建effectList

调用createInstance创建真实的 DOM,但还不会渲染到页面上

effectList将需要更新的数据形成一个新的 Fiber 链表结构,这样就只需要去更新这些

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    // Check if the work completed or if something threw.
    if ((completedWork.flags & Incomplete) === NoFlags) {
      // ...
      resetCurrentDebugFiberInDEV();

      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        workInProgress = next;
        return;
      }
    } else {
      const next = unwindWork(completedWork, subtreeRenderLanes);

      // Because this fiber did not complete, don't reset its expiration time.

      if (next !== null) {
        next.flags &= HostEffectMask;
        workInProgress = next;
        return;
      }

      // ...
    }

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null);

  // We've reached the root.
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

20. 再总结一下前面的调用栈流程:

21. commitRoot

当前面的beginWork、completeWork完成后,后续关键流程为commitWork,核心调用方法commitRoot,它其中核心调用commitRootImpl

function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediateSchedulerPriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}

22. commitRootImpl

核心调用:flushPassiveEffects、commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects

function commitRootImpl(root, renderPriorityLevel) {
  do {
    // ⭐️ 处理一些还未执行完毕的 useEffect
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

    // ...

    // ⭐️ 更新前
    commitBeforeMutationEffects(finishedWork);

    // ...

    // ⭐️ 更新时
    commitMutationEffects(finishedWork, root, renderPriorityLevel);

    if (shouldFireAfterActiveInstanceBlur) {
      afterActiveInstanceBlur();
    }
    resetAfterCommit(root.containerInfo);

    // finishedWork = workInProgressFiber
    root.current = finishedWork; //  ⭐️ 实现双缓存的切换

  
    // ⭐️ 更新后
    commitLayoutEffects(root);

    // ...

  return null;
}

a. flushPassiveEffects

处理一些还未执行完毕的 useEffect

b. commitBeforeMutationEffects(更新前)

调用getSnapshotBeforeUpdate生命周期

c. commitMutationEffects(更新时)

通过 Fiber 链表结构,一层层进行处理增删改

d. commitLayoutEffects(更新后)

进行 Layout(布局)阶段,执行一些生命周期:componentDidMount、componentWillUpdate 等、执行 setState 的 callback、调用 useLayoutEffect、将 useEffect 存储起来

React 的异步可中断(V18 版本)

中断的是下一次的 beginWork

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

shouldYield意思是交出执行权。

浏览器的任务执行顺序:宏任务 -> 微任务 -> RAF(requestAnimationFrame) -> render -> RIC(requestIdelCallback) -> 新的宏任务 -> 新的微任务 -> ...

当面对长任务时,React 会采用分段执行,但又需要满足可打断,先执行后面的,那可以使用的方案如下:

  • setTimeout:宏任务,满足 render 后再执行,但它存在 4ms 的延迟
  • Promise:微任务,不满足 render 后再执行,宏任务执行后就立马执行了
  • requestIdelCallback:在浏览器空闲时期执行,满足 render 后再执行,但它执行时间不确定,兼容性差
  • MessageChannel:浏览器创建的低延迟通信,可以创建一个宏观上的异步操作,与 render 执行无关

仿照 React 源码,模拟实现打断

// 仿照 React 源码,模拟实现打断

// 存放 任务 的队列
const queue = []

// 存放 flush 函数的队列
const transition = []

let deadTime;
const split = 5 // 任务执行超过 5ms,则让出执行权
const now = () => performance.now() // 获取当前时间,计时精度更高

const peek = arr => arr[0] // 拿出数组的第一个值

const poseMessage = (()= >{

  const { port1, port2 } = new MessageChannel()
  // ⭐️ 当 port2.postMessage 调用后,类似于在 setTimeout 后再执行 port1.ommessgae

  // 恢复执行
  port1.ommessgae = () => {
    // 取出 transition 中的第一个 flush 并执行
    peek(transition)()
  }

  // poseMessage() 执行的是这个 return 的函数
  return () => {
    port2.postMessage() // 让出执行权
  }
})()

function startTransition(flush) {
  // 将 flush 放到 transition 数组里面 && 执行 poseMessage()
  transition.push(flush) && poseMessage()
}

function shouldYield() {
  // 5ms 到了 或者 有更高优先级的操作
  return now() >= deadTime 
    || navigatior.scheduling.isInputPending // 模拟更高优先级的操作
}

// 时间分片的函数
function flush() {
  // 任务执行的截止时间为:当前时间 + 5ms
  deadTime = now() + split
  const task = peek(queue) // 拿出第一个任务,task = { task }
  while(task && !shouldYield()) {
    const { task } = task
    task.task = null // queue[0] = { task: null }
    // 执行任务
    const next = task()

    // 如果 next 是函数,则任务还未执行完,类似于 workInProgress benginWork 还未完
    if(next && typeof next === 'function') {
      // 没执行完的,重新放进去
      task.task = next // queue[0] = { task: next }
    } else {
      // 执行完了
      queue.shift()
    }
  }

  // 代码执行到这后,就两种情况:
  // 1、task 执行完了
  // 2、task 还有,但 showYield() 为 true 让出执行权了
  task && startTransition(flush)
}

// 用 schedule 来调度任务
function schedule(task) {
  queue.push( { task } ) // 将任务放进队列中

  startTransition(flush)
}

// 模拟的任务
function myTask() {
  return () => {
    console.log(1)
    
    return () => {
      console.log(2)
      
      return () => {
        console.log(3)
      
        return () => {
          console.log(4)
        }
      }
    }
  }
}

// 将任务放入队列中,开始执行
schedule(myTask)

高优先级操作

  • 用户交互事件处理
  • 错误边界
  • 引发布局或绘制的操作

Suspense 组件

本质是一个错误边界

可以作为组件的异步加载,也可以将异步请求改写为同步逻辑

const fetchList = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(["a", "b", "c", "d"]);
    }, 1000);
  });
};

const readApi = (fn) => {
  let state = "pending";
  let res;

  const suspense = fn()
    .then((r) => {
      state = "success";
      res = r;
    })
    .catch((err) => {
      state = "error";
      res = err;
    });

  function read() {
    if (state === "pending") {
      throw suspense;
    } else if (state === "error") {
      throw res;
    } else if (state === "success") {
      return res;
    }
  }

  return read;
};

const ListApi = readApi(fetchList);

const List = () => {
  // const [list, setList] = useState([]);

  // useEffect(() => {
  //   fetchList().then((res) => {
  //     setList(res);
  //   });
  // }, []);

  const list = ListApi();

  return (
    <div>
      <ul>
        {list.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default function FuncComName(props) {
  return (
    <div>
      <Suspense fallback={<div>loading...</div>}>
        <List />
      </Suspense>
    </div>
  );
}

// 逻辑解析:当第一次加载 List 组件时,走的是这个逻辑 
// 	if (state === "pending") {
//   	throw suspense;
// 	}
// 抛出了 suspense 错误,被 <Suspense 的 fallback 捕获,所以显示 loading...
// 但由于 suspense 是个 Promise,所以等它执行完毕后又重新渲染 List 组件
// 然后走的先这个逻辑
// 		else if (state === "success") {
// 			return res;
// 		}
// 所以最终又正常显示了

简易手写

class MySuspense extends Component {
  constructor(props) {
    super(props);

    this.state = {
      loading: false,
    };
  }

  componentDidCatch(error) {
    if (typeof error.then === "function") {
      this.setState({ loading: true });

      error.then(() => this.setState({ loading: false }));
    }
  }

  render() {
    return this.state.loading ? this.props.fallback : this.props.children;
  }
}

补充

SetTimeout

存在 4ms 的延迟问题

就算设置的是 0,但浏览器可能会因为其他任务(DOM 更新、事件处理等),延迟执行回调函数

所以嵌套调用越多,时间越不准确

MessageChannel

是浏览器提供的一个高级通信机制,它允许在不同上下文之间(比如窗口、iframe 或者 worker 线程)进行安全且异步的消息传递。

适用于在两个窗口之间建立直接的通信通道。

创建 MessageChannel

const messageChannel = new MessageChannel();
// 分别获取两个端口
const port1 = messageChannel.port1;
const port2 = messageChannel.port2;

发送、接受信息

// 在一个端口上接受消息
port1.onmessage = (event) => {
  console.log('收到的信息:', event.data)
}

// 在一个端口上发送消息
port1.postMessage('喂喂,收得到吗?')

BroadcastChannel

在相同源的不同上下文之间(包括但不限于标签页)实现双向、异步通信

适用于同一浏览器下的不同窗口进行广播式的通信。

创建 BroadcastChannel

// 创建一个频道名称为 "my_channel" 的 BroadcastChannel
const channel = new BroadcastChannel("my_channel");

发送、接受信息

// 接受消息
channel.onmessage = (event) => {
  console.log('收到的信息:', event.data)
}

// 发送消息
channel.postMessage('喂喂,收得到吗?')

面试题

什么是 Fiber?

本质是一个对象形式的数据结构:其中包含了虚拟 DOM、链表、EffectList 等数据

在 React 中存在 currentFiber 与 workInProgressFiber 两个 Fiber,每次的更新通过 currentFiber 与虚拟 DOM 对比去生成 workInProgressFiber,最终用 workInProgressFiber 替换掉 currentFiber,实现双缓存的替换

双缓存步骤:

  • 创建 fiberRoot 对象,将所有的 dom 串起来
  • 在通过 beginWork 的递归调用,创建出 workInProgressFiber,并打上 EffectTag
  • 然后在 completeWork 中,生成 EffectList,并创建真实的 DOM,在更新前执行一些生命周期,在更新时实现双缓存的切换,在更新后执行一些生命周期