【react18原理探究实践】React Effect List 构建与 Commit 阶段详解

在 Render 阶段,我们得到的并不是 DOM,而是一条 Effect List

那么,这些 PlacementUpdateDeletion 副作用标记,究竟是怎么落实到真实的 DOM 上的呢?

答案就是:Commit 阶段

切记、切记、切记:

  • Render 阶段:纯计算,可能被中断。
  • Commit 阶段:副作用落地,不可中断

一、Effect List 概述

1.1 什么是 Effect List?

javascript 复制代码
/* 💡 Effect List 的定义:
 * - 一个单向链表,包含所有需要执行副作用的 Fiber 节点
 * - 在 Render 阶段构建
 * - 在 Commit 阶段消费
 *
 * 为什么需要 Effect List?
 * - Fiber 树可能有成千上万个节点
 * - 但只有少数节点需要执行副作用(DOM 操作、生命周期等)
 * - Effect List 只包含这些节点,避免遍历整棵树
 */

// 旧版 React(<= 17)的 Effect List 结构:
FiberNode {
  // 指向 Effect List 的指针
  firstEffect: Fiber | null,   // 子树 Effect List 的第一个节点
  lastEffect: Fiber | null,    // 子树 Effect List 的最后一个节点
  nextEffect: Fiber | null,    // Effect List 的下一个节点

  // 副作用标记
  flags: number,               // 自身的副作用

  // 其他属性...
}

// 新版 React(>= 18)的变化:
FiberNode {
  // 不再使用 firstEffect、lastEffect、nextEffect
  // 改用 flags 和 subtreeFlags

  flags: number,               // 自身的副作用
  subtreeFlags: number,        // 子树的副作用(所有子节点 flags 的集合)

  // Commit 阶段直接遍历 Fiber 树,根据 flags 判断是否需要处理
}

1.2 Effect List 在整个流程中的位置

sql 复制代码
React 渲染流程:

触发更新
    ↓
Render 阶段开始
    ↓
beginWork(递阶段)
    ├─ 创建/更新 Fiber 节点
    ├─ 标记 flags(Placement、Update 等)
    └─ 返回子节点
    ↓
completeWork(归阶段)
    ├─ 创建/更新 DOM
    ├─ 🔥 收集副作用(bubbleProperties)
    │   ├─ 收集子节点的 flags
    │   ├─ 合并到 subtreeFlags
    │   └─ 构建 Effect List(旧版)
    └─ 返回父节点
    ↓
Render 阶段完成
    ↓
rootFiber.firstEffect 指向 Effect List 头(旧版)
rootFiber.subtreeFlags 包含所有副作用(新版)
    ↓
Commit 阶段开始
    ↓
遍历 Effect List / Fiber 树
    ├─ 执行 DOM 操作
    ├─ 调用生命周期
    └─ 执行 useEffect
    ↓
Commit 阶段完成
    ↓
屏幕更新

二、副作用的类型(Flags)

2.1 常见的 Flags 定义

javascript 复制代码
// 📍 位置:ReactFiberFlags.js

export const NoFlags = 0b0000000000000000000000000000;

// 🔥 Fiber 节点自身的副作用
export const PerformedWork = 0b0000000000000000000000000001;
export const Placement = 0b0000000000000000000000000010; // 插入
export const Update = 0b0000000000000000000000000100; // 更新
export const Deletion = 0b0000000000000000000000001000; // 删除
export const ContentReset = 0b0000000000000000000000010000;
export const Callback = 0b0000000000000000000000100000;
export const DidCapture = 0b0000000000000000000001000000;
export const Ref = 0b0000000000000000000010000000; // Ref 需要更新
export const Snapshot = 0b0000000000000000000100000000; // getSnapshotBeforeUpdate
export const Passive = 0b0000000000000000001000000000; // useEffect
export const Hydrating = 0b0000000000000000010000000000;
export const Visibility = 0b0000000000000000100000000000;
export const StoreConsistency = 0b0000000000000001000000000000;

// 🔥 生命周期相关
export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot;

// 🔥 Host 副作用(DOM 操作)
export const HostEffectMask = 0b0000000000000000011111111111;

// 🔥 所有副作用
export const BeforeMutationMask = Update | Snapshot;
export const MutationMask =
  Placement | Update | Deletion | ContentReset | Ref | Hydrating | Visibility;
export const LayoutMask = Update | Callback | Ref | Visibility;

2.2 Flags 的使用方式

javascript 复制代码
// 添加 flag
fiber.flags |= Placement;
fiber.flags |= Update;

// 检查 flag
if (fiber.flags & Placement) {
  // 节点需要插入
}

if (fiber.flags & Update) {
  // 节点需要更新
}

// 移除 flag
fiber.flags &= ~Placement;

// 检查是否有任何副作用
if (fiber.flags !== NoFlags) {
  // 节点有副作用
}

// 示例:一个节点可能同时有多个 flags
fiber.flags = Placement | Update | Ref;
// 二进制:0b0000000000000000000010000110

2.3 不同阶段的 Flags

javascript 复制代码
// ========== Render 阶段标记 ==========

// beginWork 中标记:
case ClassComponent: {
  if (instance === null) {
    // 首次挂载,标记 Placement
    workInProgress.flags |= Placement;
  }

  if (typeof instance.componentDidMount === 'function') {
    // 标记 Update(生命周期)
    workInProgress.flags |= Update;
  }
}

// reconcileChildren 中标记:
if (shouldTrackSideEffects) {
  if (current === null) {
    // 新增节点
    newFiber.flags |= Placement;
  } else {
    // 更新节点
    if (oldProps !== newProps) {
      newFiber.flags |= Update;
    }
  }
}

// completeWork 中标记:
if (workInProgress.ref !== null) {
  // Ref 需要更新
  workInProgress.flags |= Ref;
}


// ========== Commit 阶段消费 ==========

// Before Mutation 阶段:
if (fiber.flags & Snapshot) {
  // 调用 getSnapshotBeforeUpdate
}

// Mutation 阶段:
if (fiber.flags & Placement) {
  // 插入 DOM
  commitPlacement(fiber);
}

if (fiber.flags & Update) {
  // 更新 DOM 属性
  commitWork(fiber);
}

if (fiber.flags & Deletion) {
  // 删除 DOM
  commitDeletion(fiber);
}

// Layout 阶段:
if (fiber.flags & Update) {
  // 调用 componentDidMount/componentDidUpdate
  commitLifeCycles(fiber);
}

if (fiber.flags & Callback) {
  // 调用 setState 的 callback
}

三、Effect List 的构建过程

3.1 completeWork 中的副作用收集

javascript 复制代码
// 📍 位置:ReactFiberCompleteWork.new.js

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  // ... 创建 DOM、更新属性等

  // 🔥 关键:在返回前收集副作用
  bubbleProperties(workInProgress);

  return null;
}

3.2 旧版 Effect List 的构建(React <= 17)

javascript 复制代码
// 📍 位置:ReactFiberWorkLoop.old.js(旧版)

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;

  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    // 调用 completeWork
    let next = completeWork(current, completedWork, subtreeRenderLanes);

    if (next !== null) {
      workInProgress = next;
      return;
    }

    // ========== 🔥 构建 Effect List 🔥 ==========

    if (
      returnFiber !== null &&
      (completedWork.flags & Incomplete) === NoFlags
    ) {
      // 步骤1:将子节点的 Effect List 附加到父节点
      if (returnFiber.firstEffect === null) {
        // 父节点还没有 Effect List,直接使用子节点的
        returnFiber.firstEffect = completedWork.firstEffect;
      }

      if (completedWork.lastEffect !== null) {
        // 子节点有 Effect List
        if (returnFiber.lastEffect !== null) {
          // 父节点已经有 Effect List,连接起来
          returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
        }
        // 更新父节点的 lastEffect
        returnFiber.lastEffect = completedWork.lastEffect;
      }

      // 步骤2:将当前节点加入 Effect List(如果有副作用)
      const flags = completedWork.flags;

      // 跳过 PerformedWork 和 Incomplete
      if (flags > PerformedWork) {
        if (returnFiber.lastEffect !== null) {
          // 连接到父节点的 Effect List 末尾
          returnFiber.lastEffect.nextEffect = completedWork;
        } else {
          // 父节点的 Effect List 是空的
          returnFiber.firstEffect = completedWork;
        }
        // 更新 lastEffect
        returnFiber.lastEffect = completedWork;
      }
    }

    // 继续处理兄弟节点或回溯
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }

    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
}

Effect List 构建的可视化:

markdown 复制代码
Fiber 树结构:
       App
      /   \
    div   Content
    /
  header

假设各节点的 flags:
- App: Update (componentDidMount)
- div: NoFlags
- header: Placement (新增)
- Content: Update (componentDidUpdate)


构建过程(从下往上):

1. header completeWork:
   - header.flags = Placement
   - header.firstEffect = null
   - header.lastEffect = null

2. header 添加到 div 的 Effect List:
   - div.firstEffect = header
   - div.lastEffect = header
   - 因为 header.flags > PerformedWork

3. Content completeWork:
   - Content.flags = Update
   - Content.firstEffect = null
   - Content.lastEffect = null

4. Content 添加到 App 的 Effect List:
   - App.firstEffect = Content
   - App.lastEffect = Content

5. div 回溯到 App:
   - div 的子树 Effect List: header
   - div 自身无副作用 (flags = NoFlags)
   - 合并:
     - App.lastEffect.nextEffect = div.firstEffect (Content.nextEffect = header)
     - App.lastEffect = div.lastEffect (App.lastEffect = header)

6. App 添加自己:
   - App.flags = Update
   - App.lastEffect.nextEffect = App (header.nextEffect = App)
   - App.lastEffect = App

最终 Effect List:
App.firstEffect → Content → header → App → null
                     ↓         ↓       ↓
                  nextEffect nextEffect nextEffect

Commit 阶段按顺序执行:
1. Content (Update)
2. header (Placement)
3. App (Update)

四、bubbleProperties - 副作用冒泡

4.1 新版实现(React >= 18)

javascript 复制代码
// 📍 位置:ReactFiberCompleteWork.new.js

function bubbleProperties(completedWork: Fiber) {
  /* 💡 新版的副作用收集策略:
   * - 不再构建 Effect List 链表
   * - 使用 subtreeFlags 收集子树的所有副作用
   * - Commit 阶段遍历 Fiber 树,根据 flags 和 subtreeFlags 判断
   */

  const didBailout =
    completedWork.alternate !== null &&
    completedWork.alternate.child === completedWork.child;

  let newChildLanes = NoLanes;
  let subtreeFlags = NoFlags;

  if (!didBailout) {
    // === 首次渲染或有更新 ===

    let child = completedWork.child;
    while (child !== null) {
      // 🔥 收集子节点的 lanes
      newChildLanes = mergeLanes(
        newChildLanes,
        mergeLanes(child.lanes, child.childLanes)
      );

      // 🔥 收集子节点的 flags
      subtreeFlags |= child.subtreeFlags;
      subtreeFlags |= child.flags;

      // 设置 return 指针
      child.return = completedWork;
      child = child.sibling;
    }

    // 🔥 将收集的副作用设置到当前节点
    completedWork.subtreeFlags |= subtreeFlags;
  } else {
    // === Bailout 优化路径 ===

    let child = completedWork.child;
    while (child !== null) {
      newChildLanes = mergeLanes(
        newChildLanes,
        mergeLanes(child.lanes, child.childLanes)
      );

      // Bailout 时只收集静态副作用
      subtreeFlags |= child.subtreeFlags & StaticMask;
      subtreeFlags |= child.flags & StaticMask;

      child.return = completedWork;
      child = child.sibling;
    }

    completedWork.subtreeFlags |= subtreeFlags;
  }

  completedWork.childLanes = newChildLanes;

  return didBailout;
}

4.2 subtreeFlags 的使用

javascript 复制代码
// Render 阶段结束后:

rootFiber {
  flags: NoFlags,
  subtreeFlags: Placement | Update | Ref,  // 子树有这些副作用
  child: AppFiber,
}
  ↓
AppFiber {
  flags: Update,  // componentDidMount
  subtreeFlags: Placement | Ref,  // 子树有这些副作用
  child: divFiber,
}
  ↓
divFiber {
  flags: NoFlags,
  subtreeFlags: Placement,  // 子树有插入操作
  child: headerFiber,
}
  ↓
headerFiber {
  flags: Placement,  // 新增节点
  subtreeFlags: NoFlags,  // 叶子节点
}


// Commit 阶段使用:

function commitMutationEffects(root, finishedWork) {
  // 递归遍历 Fiber 树
  commitMutationEffectsOnFiber(finishedWork, root);
}

function commitMutationEffectsOnFiber(finishedWork, root) {
  const flags = finishedWork.flags;

  // 🔥 先处理子树
  if (finishedWork.subtreeFlags & MutationMask) {
    // 子树有需要处理的副作用,递归处理
    let child = finishedWork.child;
    while (child !== null) {
      commitMutationEffectsOnFiber(child, root);
      child = child.sibling;
    }
  }

  // 🔥 再处理自己
  if (flags & MutationMask) {
    // 自己有副作用,执行对应操作
    switch (finishedWork.tag) {
      case HostComponent: {
        if (flags & Placement) {
          commitPlacement(finishedWork);
        }
        if (flags & Update) {
          commitWork(current, finishedWork);
        }
        // ...
      }
    }
  }
}

五、完整的 Effect List 构建示例

5.1 示例代码

jsx 复制代码
class App extends React.Component {
  state = { show: true };

  componentDidMount() {
    console.log("App mounted");
  }

  render() {
    return (
      <div>
        <header ref={this.headerRef}>Header</header>
        {this.state.show && <Content />}
      </div>
    );
  }
}

class Content extends React.Component {
  componentDidUpdate() {
    console.log("Content updated");
  }

  render() {
    return (
      <div>
        <p>Text 1</p>
        <p>Text 2</p>
      </div>
    );
  }
}

// 首次渲染
ReactDOM.render(<App />, root);

5.2 首次渲染的 Effect 收集

ini 复制代码
=== Render 阶段(归阶段)===

1. p("Text 1") completeWork:
   flags: Placement
   subtreeFlags: NoFlags
   firstEffect: null
   lastEffect: null

2. p("Text 2") completeWork:
   flags: Placement
   subtreeFlags: NoFlags

3. div (Content's child) completeWork:
   flags: NoFlags

   bubbleProperties:
   - child1 = p("Text 1"): flags = Placement
   - child2 = p("Text 2"): flags = Placement
   - subtreeFlags = Placement | Placement = Placement

   结果:
   div.subtreeFlags = Placement

   Effect List (旧版):
   div.firstEffect = p("Text 1")
   p("Text 1").nextEffect = p("Text 2")
   div.lastEffect = p("Text 2")

4. Content completeWork:
   flags: Update (componentDidMount)

   bubbleProperties:
   - child = div: subtreeFlags = Placement
   - subtreeFlags = Placement

   结果:
   Content.subtreeFlags = Placement
   Content.flags = Update

   Effect List (旧版):
   Content.firstEffect = p("Text 1")
   Content.lastEffect = p("Text 2")

   Content 自己加入:
   p("Text 2").nextEffect = Content
   Content.lastEffect = Content

5. header completeWork:
   flags: Placement | Ref
   subtreeFlags: NoFlags

6. div (App's child) completeWork:
   flags: NoFlags

   bubbleProperties:
   - child1 = header: flags = Placement | Ref
   - child2 = Content: flags = Update, subtreeFlags = Placement
   - subtreeFlags = (Placement | Ref) | Update | Placement

   结果:
   div.subtreeFlags = Placement | Ref | Update

   Effect List (旧版):
   div.firstEffect = header
   header.nextEffect = p("Text 1")
   p("Text 1").nextEffect = p("Text 2")
   p("Text 2").nextEffect = Content
   div.lastEffect = Content

7. App completeWork:
   flags: Update (componentDidMount)

   bubbleProperties:
   - child = div: subtreeFlags = Placement | Ref | Update
   - subtreeFlags = Placement | Ref | Update

   结果:
   App.subtreeFlags = Placement | Ref | Update
   App.flags = Update

   Effect List (旧版):
   App.firstEffect = header
   header.nextEffect = p("Text 1")
   p("Text 1").nextEffect = p("Text 2")
   p("Text 2").nextEffect = Content
   Content.nextEffect = App
   App.lastEffect = App

8. HostRootFiber completeWork:
   flags: NoFlags

   bubbleProperties:
   - child = App: flags = Update, subtreeFlags = Placement | Ref | Update
   - subtreeFlags = Update | Placement | Ref | Update

   结果:
   HostRootFiber.subtreeFlags = Placement | Ref | Update

   Effect List (旧版):
   HostRootFiber.firstEffect = header
   header.nextEffect = p("Text 1")
   p("Text 1").nextEffect = p("Text 2")
   p("Text 2").nextEffect = Content
   Content.nextEffect = App
   HostRootFiber.lastEffect = App


=== 最终的 Effect List(旧版)===

HostRootFiber.firstEffect → header → p1 → p2 → Content → App → null
                              ↓       ↓    ↓      ↓        ↓
                          Placement Plac Plac  Update   Update
                          + Ref     ement ement


=== 最终的 subtreeFlags(新版)===

HostRootFiber: subtreeFlags = Placement | Ref | Update
    ↓
App: flags = Update, subtreeFlags = Placement | Ref | Update
    ↓
div: flags = NoFlags, subtreeFlags = Placement | Ref | Update
    ├─ header: flags = Placement | Ref
    └─ Content: flags = Update, subtreeFlags = Placement
        ↓
       div: flags = NoFlags, subtreeFlags = Placement
         ├─ p1: flags = Placement
         └─ p2: flags = Placement

六、Commit 阶段的三个子阶段

6.1 Commit 阶段概述

javascript 复制代码
// 📍 位置:ReactFiberWorkLoop.new.js

function commitRoot(root) {
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;

  // 重置标记
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  // 🔥 Commit 阶段的三个子阶段

  // 1. Before Mutation 阶段
  commitBeforeMutationEffects(root, finishedWork);

  // 2. Mutation 阶段
  commitMutationEffects(root, finishedWork, lanes);

  // 3. Layout 阶段
  commitLayoutEffects(finishedWork, root, lanes);

  // 切换 current 指针
  root.current = finishedWork;
}

6.2 Before Mutation 阶段

javascript 复制代码
function commitBeforeMutationEffects(root: FiberRoot, firstChild: Fiber) {
  /* 💡 Before Mutation 阶段的任务:
   * 1. 调用 getSnapshotBeforeUpdate
   * 2. 异步调度 useEffect
   */

  // 遍历 Effect List(旧版)或 Fiber 树(新版)
  let fiber = firstChild;
  while (fiber !== null) {
    if (fiber.flags & Snapshot) {
      // 🔥 调用 getSnapshotBeforeUpdate
      commitBeforeMutationEffectOnFiber(fiber);
    }

    if (fiber.flags & Passive) {
      // 🔥 调度 useEffect
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
      });
    }

    fiber = fiber.nextEffect; // 旧版
    // 或遍历子树  // 新版
  }
}

function commitBeforeMutationEffectOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  switch (finishedWork.tag) {
    case ClassComponent: {
      if (flags & Snapshot) {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;

          // 🔥 调用 getSnapshotBeforeUpdate
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState
          );

          // 保存 snapshot,传给 componentDidUpdate
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
      }
      break;
    }
  }
}

6.3 Mutation 阶段

javascript 复制代码
function commitMutationEffects(
  root: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes
) {
  /* 💡 Mutation 阶段的任务:
   * 1. 执行 DOM 操作(插入、更新、删除)
   * 2. 调用 useLayoutEffect 的销毁函数
   * 3. 更新 ref
   */

  // 遍历 Fiber 树
  commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
}

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  // 🔥 先处理子树
  if (finishedWork.subtreeFlags & MutationMask) {
    let child = finishedWork.child;
    while (child !== null) {
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }

  // 🔥 再处理自己
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      // 调用 useLayoutEffect 的销毁函数
      if (flags & Update) {
        commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
      }
      break;
    }

    case HostComponent: {
      const instance = finishedWork.stateNode;

      if (flags & Placement) {
        // 🔥 插入 DOM
        const parent = getHostParentFiber(finishedWork);
        const before = getHostSibling(finishedWork);
        insertOrAppendPlacementNode(finishedWork, before, parent);
      }

      if (flags & Update) {
        // 🔥 更新 DOM 属性
        if (instance != null && current !== null) {
          const updatePayload = finishedWork.updateQueue;
          finishedWork.updateQueue = null;

          if (updatePayload !== null) {
            commitUpdate(
              instance,
              updatePayload,
              finishedWork.type,
              current.memoizedProps,
              finishedWork.memoizedProps,
              finishedWork
            );
          }
        }
      }

      if (flags & Ref) {
        // 🔥 清空旧 ref
        if (current !== null && current.ref !== null) {
          commitDetachRef(current);
        }
      }

      break;
    }
  }

  // 🔥 处理删除
  if (flags & Deletion) {
    const deletions = finishedWork.deletions;
    if (deletions !== null) {
      for (let i = 0; i < deletions.length; i++) {
        const childToDelete = deletions[i];
        commitDeletionEffects(root, finishedWork, childToDelete);
      }
    }
  }
}

6.4 Layout 阶段

javascript 复制代码
function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes
) {
  /* 💡 Layout 阶段的任务:
   * 1. 调用生命周期方法(componentDidMount、componentDidUpdate)
   * 2. 调用 useLayoutEffect 的创建函数
   * 3. 更新 ref
   * 4. 调用 setState 的 callback
   */

  // 遍历 Fiber 树
  commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
}

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes
) {
  const flags = finishedWork.flags;

  switch (finishedWork.tag) {
    case ClassComponent: {
      const instance = finishedWork.stateNode;

      if (flags & Update) {
        if (current === null) {
          // 🔥 首次挂载,调用 componentDidMount
          instance.componentDidMount();
        } else {
          // 🔥 更新,调用 componentDidUpdate
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const snapshot = instance.__reactInternalSnapshotBeforeUpdate;

          instance.componentDidUpdate(prevProps, prevState, snapshot);
        }
      }

      if (flags & Callback) {
        // 🔥 调用 setState 的 callback
        const updateQueue = finishedWork.updateQueue;
        if (updateQueue !== null) {
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
      }

      break;
    }

    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      if (flags & Update) {
        // 🔥 调用 useLayoutEffect 的创建函数
        commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
      }

      break;
    }

    case HostComponent: {
      if (flags & Ref) {
        // 🔥 更新 ref
        commitAttachRef(finishedWork);
      }

      break;
    }
  }

  // 递归处理子树
  if (finishedWork.subtreeFlags & LayoutMask) {
    let child = finishedWork.child;
    while (child !== null) {
      commitLayoutEffectOnFiber(
        finishedRoot,
        child.alternate,
        child,
        committedLanes
      );
      child = child.sibling;
    }
  }
}

七、Effect List 的遍历和消费

7.1 旧版的遍历方式

javascript 复制代码
// 📍 位置:ReactFiberWorkLoop.old.js(React <= 17)

function commitRootImpl(root, renderPriorityLevel) {
  const finishedWork = root.finishedWork;

  // 获取 Effect List
  let firstEffect = finishedWork.firstEffect;

  if (firstEffect !== null) {
    // ========== Before Mutation 阶段 ==========
    let nextEffect = firstEffect;
    do {
      try {
        commitBeforeMutationEffects();
      } catch (error) {
        // 错误处理
        captureCommitPhaseError(nextEffect, error);
        nextEffect = nextEffect.nextEffect;
      }
    } while (nextEffect !== null);

    // ========== Mutation 阶段 ==========
    nextEffect = firstEffect;
    do {
      try {
        commitMutationEffects(root, renderPriorityLevel);
      } catch (error) {
        captureCommitPhaseError(nextEffect, error);
        nextEffect = nextEffect.nextEffect;
      }
    } while (nextEffect !== null);

    // 切换 current 树
    root.current = finishedWork;

    // ========== Layout 阶段 ==========
    nextEffect = firstEffect;
    do {
      try {
        commitLayoutEffects(root, lanes);
      } catch (error) {
        captureCommitPhaseError(nextEffect, error);
        nextEffect = nextEffect.nextEffect;
      }
    } while (nextEffect !== null);

    // 清空 nextEffect
    nextEffect = null;
  }
}

function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    const current = nextEffect.alternate;
    const flags = nextEffect.flags;

    if ((flags & Snapshot) !== NoFlags) {
      // 处理 getSnapshotBeforeUpdate
      commitBeforeMutationEffectOnFiber(current, nextEffect);
    }

    if ((flags & Passive) !== NoFlags) {
      // 调度 useEffect
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
      });
    }

    nextEffect = nextEffect.nextEffect;
  }
}

7.2 新版的遍历方式

javascript 复制代码
// 📍 位置:ReactFiberWorkLoop.new.js(React >= 18)

function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority
) {
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;

  // ========== Before Mutation 阶段 ==========
  const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
    root,
    finishedWork
  );

  // ========== Mutation 阶段 ==========
  commitMutationEffects(root, finishedWork, lanes);

  // 切换 current 树
  root.current = finishedWork;

  // ========== Layout 阶段 ==========
  commitLayoutEffects(finishedWork, root, lanes);

  // 请求绘制
  requestPaint();
}

// 新版不使用 Effect List,而是递归遍历 Fiber 树
function commitMutationEffects(
  root: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes
) {
  // 递归遍历整个 Fiber 树
  commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
}

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  // 🔥 优化:先检查 subtreeFlags
  if (finishedWork.subtreeFlags & MutationMask) {
    // 子树有副作用,递归处理
    let child = finishedWork.child;
    while (child !== null) {
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }

  // 🔥 处理自己的副作用
  if (flags & MutationMask) {
    // 执行 DOM 操作
    switch (finishedWork.tag) {
      case HostComponent:
        if (flags & Placement) {
          commitPlacement(finishedWork);
        }
        if (flags & Update) {
          commitWork(current, finishedWork);
        }
      // ...
    }
  }
}

7.3 两种方式的对比

javascript 复制代码
// ========== 旧版(Effect List)==========

// 优点:
// 1. 只遍历有副作用的节点,性能更好
// 2. 跳过无副作用的子树

// 缺点:
// 1. 需要维护额外的链表结构
// 2. 增加了 Render 阶段的复杂度
// 3. 占用更多内存(firstEffect、lastEffect、nextEffect 指针)

let effect = root.firstEffect;
while (effect !== null) {
  // 只处理有副作用的节点
  commitWork(effect);
  effect = effect.nextEffect;
}

// ========== 新版(subtreeFlags)==========

// 优点:
// 1. 不需要额外的链表结构
// 2. Render 阶段更简单
// 3. 节省内存

// 缺点:
// 1. 需要递归遍历 Fiber 树
// 2. 可能会访问一些无副作用的节点(但可以通过 subtreeFlags 剪枝)

function commitWork(fiber) {
  // 先检查是否需要处理子树
  if (fiber.subtreeFlags & SomeMask) {
    let child = fiber.child;
    while (child !== null) {
      commitWork(child); // 递归
      child = child.sibling;
    }
  }

  // 处理自己
  if (fiber.flags & SomeMask) {
    // 执行副作用
  }
}

八、完整的 Commit 流程示例

8.1 示例:状态更新

jsx 复制代码
class App extends React.Component {
  state = { count: 0 };

  headerRef = React.createRef();

  componentDidMount() {
    console.log("App mounted");
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("App updated", snapshot);
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("Before update", prevState.count, "->", this.state.count);
    return { prevCount: prevState.count };
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <header ref={this.headerRef}>Count: {this.state.count}</header>
        <button onClick={this.handleClick}>+1</button>
      </div>
    );
  }
}

8.2 点击按钮后的完整流程

markdown 复制代码
=== 触发更新 ===

1. 点击 button
2. 调用 handleClick
3. 执行 setState({ count: 1 })
4. 创建 update 对象,放入 updateQueue
5. 调度更新


=== Render 阶段 ===

1. beginWork(AppFiber)
   - processUpdateQueue: 计算新 state {count: 1}
   - instance.render()
   - reconcileChildren
   - 发现 header 的 props 变化:{children: "Count: 0"} → {children: "Count: 1"}
   - headerFiber.flags |= Update

2. beginWork(headerFiber)
   - 文本变化,需要更新
   - 继续...

3. completeWork(headerFiber)
   - headerFiber.flags = Update
   - 创建 updatePayload: ["children", "Count: 1"]
   - headerFiber.updateQueue = updatePayload

4. completeWork(divFiber)
   - bubbleProperties:
     - child1(header).flags = Update
     - subtreeFlags = Update
   - divFiber.subtreeFlags = Update

5. completeWork(AppFiber)
   - AppFiber.flags = Update | Snapshot
     - Update: componentDidUpdate
     - Snapshot: getSnapshotBeforeUpdate
   - bubbleProperties:
     - child(div).subtreeFlags = Update
     - subtreeFlags = Update
   - AppFiber.subtreeFlags = Update

6. Render 阶段完成
   - HostRootFiber.subtreeFlags = Update | Snapshot


=== Commit 阶段 ===

// Before Mutation 阶段:

1. 遍历到 AppFiber:
   - flags & Snapshot ✓
   - 调用 getSnapshotBeforeUpdate(prevProps, prevState)
   - 控制台输出:"Before update 0 -> 1"
   - 保存 snapshot: instance.__reactInternalSnapshotBeforeUpdate = {prevCount: 0}


// Mutation 阶段:

1. 切换到 HostRootFiber
2. 递归到 divFiber:
   - divFiber.subtreeFlags & MutationMask ✓
   - 继续递归

3. 递归到 headerFiber:
   - headerFiber.flags & Update ✓
   - commitWork(headerFiber):
     - updatePayload = ["children", "Count: 1"]
     - updateDOMProperties(headerDOM, updatePayload)
     - headerDOM.textContent = "Count: 1"
   - 🔥 DOM 已更新!

4. 切换 current 指针:
   - root.current = finishedWork
   - 新的 Fiber 树成为 current 树


// Layout 阶段:

1. 递归到 AppFiber:
   - flags & Update ✓
   - 调用 componentDidUpdate:
     - prevProps: {}
     - prevState: {count: 0}
     - snapshot: {prevCount: 0}
   - 控制台输出:"App updated {prevCount: 0}"

2. 递归到 headerFiber:
   - flags & Ref ✓
   - commitAttachRef(headerFiber):
     - this.headerRef.current = headerDOM


=== Commit 完成 ===

- 屏幕显示 "Count: 1"
- 控制台输出:
  "Before update 0 -> 1"
  "App updated {prevCount: 0}"

九、新旧版本的差异

9.1 数据结构的变化

javascript 复制代码
// ========== React 17 及之前 ==========

type Fiber = {
  // Effect List 相关
  firstEffect: Fiber | null,   // 子树 Effect List 的头
  lastEffect: Fiber | null,    // 子树 Effect List 的尾
  nextEffect: Fiber | null,    // 下一个有副作用的节点

  // 副作用标记
  flags: Flags,                // 自身的副作用

  // 其他字段...
};

// Effect List 的结构:
root.firstEffect → fiber1 → fiber2 → fiber3 → null
                      ↓        ↓        ↓
                  nextEffect nextEffect nextEffect


// ========== React 18 及之后 ==========

type Fiber = {
  // 移除了 Effect List
  // firstEffect: 删除
  // lastEffect: 删除
  // nextEffect: 删除

  // 新增 subtreeFlags
  flags: Flags,                // 自身的副作用
  subtreeFlags: Flags,         // 子树的副作用(子节点 flags 的并集)

  // 其他字段...
};

// 不再使用链表,直接遍历 Fiber 树

9.2 构建方式的变化

javascript 复制代码
// ========== React 17 及之前 ==========

function completeUnitOfWork(unitOfWork) {
  // ...

  if (returnFiber !== null) {
    // 🔥 手动构建 Effect List
    if (returnFiber.firstEffect === null) {
      returnFiber.firstEffect = completedWork.firstEffect;
    }

    if (completedWork.lastEffect !== null) {
      if (returnFiber.lastEffect !== null) {
        returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
      }
      returnFiber.lastEffect = completedWork.lastEffect;
    }

    if (flags > PerformedWork) {
      if (returnFiber.lastEffect !== null) {
        returnFiber.lastEffect.nextEffect = completedWork;
      } else {
        returnFiber.firstEffect = completedWork;
      }
      returnFiber.lastEffect = completedWork;
    }
  }
}

// ========== React 18 及之后 ==========

function bubbleProperties(completedWork) {
  // 🔥 只需要收集 flags
  let subtreeFlags = NoFlags;
  let child = completedWork.child;

  while (child !== null) {
    subtreeFlags |= child.subtreeFlags;
    subtreeFlags |= child.flags;
    child = child.sibling;
  }

  completedWork.subtreeFlags |= subtreeFlags;
}

9.3 消费方式的变化

javascript 复制代码
// ========== React 17 及之前 ==========

function commitMutationEffects() {
  // 🔥 遍历 Effect List
  while (nextEffect !== null) {
    const flags = nextEffect.flags;

    if (flags & Placement) {
      commitPlacement(nextEffect);
    }

    if (flags & Update) {
      commitWork(current, nextEffect);
    }

    nextEffect = nextEffect.nextEffect;
  }
}

// ========== React 18 及之后 ==========

function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
  // 🔥 递归遍历 Fiber 树

  // 先处理子树
  if (finishedWork.subtreeFlags & MutationMask) {
    let child = finishedWork.child;
    while (child !== null) {
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }

  // 再处理自己
  const flags = finishedWork.flags;

  if (flags & Placement) {
    commitPlacement(finishedWork);
  }

  if (flags & Update) {
    commitWork(current, finishedWork);
  }
}

十、常见问题深度解答

Q1: 为什么要从 Effect List 改成 subtreeFlags?

深度解答:

javascript 复制代码
// Effect List 的问题:

1. 内存开销
   - 每个 Fiber 需要 3 个额外指针:firstEffect、lastEffect、nextEffect
   - 假设 10000 个 Fiber,就是 30000 个指针
   - 每个指针 8 字节,总共 240KB

2. 构建复杂度
   - 需要在 completeUnitOfWork 中手动维护链表
   - 代码逻辑复杂,容易出错

3. 不利于并发模式
   - Effect List 是全局的、线性的
   - 在并发模式中,可能需要中断和恢复
   - Effect List 的维护变得更加困难


// subtreeFlags 的优势:

1. 更少的内存
   - 只需要一个 subtreeFlags 字段(32 位整数)
   - 10000 个 Fiber 只需要 40KB

2. 更简单的实现
   - 只需要按位或操作:subtreeFlags |= child.flags
   - 代码更简洁,更不容易出错

3. 更好的并发支持
   - 可以随时中断和恢复
   - 不需要维护全局的链表状态

4. 更灵活的优化
   - 可以根据 subtreeFlags 决定是否需要遍历子树
   - 实现更细粒度的剪枝


// 性能对比:

// Effect List(旧版)
// 优点:只访问有副作用的节点
// 缺点:构建链表有开销

let effect = root.firstEffect;
let count = 0;
while (effect !== null) {
  count++;
  effect = effect.nextEffect;
}
// 假设有 100 个有副作用的节点,访问 100 次


// subtreeFlags(新版)
// 优点:构建简单,内存小
// 缺点:可能访问一些无副作用的节点

function traverse(fiber, count = 0) {
  if (fiber.subtreeFlags & SomeMask) {
    let child = fiber.child;
    while (child !== null) {
      count = traverse(child, count);
      child = child.sibling;
    }
  }

  if (fiber.flags & SomeMask) {
    count++;
  }

  return count;
}
// 假设 Fiber 树有 10000 个节点,但只有 100 个有副作用
// 最坏情况:访问所有 10000 个节点
// 最好情况:通过 subtreeFlags 剪枝,只访问几百个节点


// 实际测试表明:
// - 构建 Effect List 的开销 > subtreeFlags 的遍历开销
// - 内存节省显著
// - 代码简洁性提升
// - 因此 React 18 选择了 subtreeFlags

Q2: Commit 阶段为什么要分三个子阶段?

深度解答:

javascript 复制代码
// 三个阶段的必要性:

// ========== Before Mutation 阶段 ==========
// 时机:DOM 变更之前
// 作用:
// 1. 调用 getSnapshotBeforeUpdate
//    - 可以读取变更前的 DOM 状态
//    - 例如:保存滚动位置
// 2. 调度 useEffect
//    - 异步执行,不阻塞渲染

class ScrollList extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 🔥 在 DOM 更新前,读取滚动位置
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 🔥 在 DOM 更新后,恢复滚动位置
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }
}


// ========== Mutation 阶段 ==========
// 时机:执行 DOM 变更
// 作用:
// 1. 插入、更新、删除 DOM
// 2. 调用 useLayoutEffect 的销毁函数
//    - 需要在 DOM 变更时执行

function Example() {
  const [width, setWidth] = useState(0);
  const ref = useRef();

  useLayoutEffect(() => {
    // 🔥 在 DOM 更新后,重新测量
    setWidth(ref.current.offsetWidth);

    return () => {
      // 🔥 在 DOM 更新前,清理
      console.log('cleanup');
    };
  });

  return <div ref={ref}>Width: {width}</div>;
}


// ========== Layout 阶段 ==========
// 时机:DOM 变更之后,浏览器绘制之前
// 作用:
// 1. 调用 componentDidMount/componentDidUpdate
// 2. 调用 useLayoutEffect 的创建函数
// 3. 更新 ref.current
// 4. 调用 setState 的 callback

// 为什么要在 Layout 阶段更新 ref?
class Parent extends React.Component {
  childRef = React.createRef();

  componentDidMount() {
    // 🔥 此时 ref 已经更新
    console.log(this.childRef.current);  // <div>Child</div>
  }

  render() {
    return <Child ref={this.childRef} />;
  }
}


// 如果只有一个阶段会怎样?

// 错误示例:所有操作在一个阶段
commitEffects() {
  // 1. 更新 DOM
  updateDOM();

  // 2. 调用 getSnapshotBeforeUpdate
  // ❌ 太晚了!DOM 已经更新了
  getSnapshotBeforeUpdate();

  // 3. 调用 componentDidUpdate
  componentDidUpdate();
}


// 正确示例:分三个阶段
commitBeforeMutation() {
  // ✅ DOM 更新前调用
  getSnapshotBeforeUpdate();
}

commitMutation() {
  // ✅ 更新 DOM
  updateDOM();
}

commitLayout() {
  // ✅ DOM 更新后调用
  componentDidUpdate();
}

Q3: 副作用的执行顺序是怎样的?

深度解答:

javascript 复制代码
// 示例代码:
class Parent extends React.Component {
  componentDidMount() {
    console.log("Parent didMount");
  }

  componentDidUpdate() {
    console.log("Parent didUpdate");
  }

  render() {
    return (
      <div>
        <Child1 />
        <Child2 />
      </div>
    );
  }
}

class Child1 extends React.Component {
  componentDidMount() {
    console.log("Child1 didMount");
  }

  render() {
    return <div>Child1</div>;
  }
}

class Child2 extends React.Component {
  componentDidMount() {
    console.log("Child2 didMount");
  }

  render() {
    return <div>Child2</div>;
  }
}

// ========== 首次渲染的执行顺序 ==========

// Render 阶段(深度优先遍历):
// 1. Parent beginWork
// 2. div beginWork
// 3. Child1 beginWork
// 4. Child1 completeWork  → 标记 flags
// 5. Child2 beginWork
// 6. Child2 completeWork  → 标记 flags
// 7. div completeWork     → 收集子树 flags
// 8. Parent completeWork  → 收集子树 flags

// Effect List(旧版):
// Child1 → Child2 → Parent

// Commit 阶段(按 Effect List 顺序):
// 输出:
// "Child1 didMount"
// "Child2 didMount"
// "Parent didMount"

// 🔥 结论:子组件的生命周期先执行,父组件后执行

// ========== 更新时的执行顺序 ==========

// 假设更新 Parent 的 state

// Render 阶段:
// 1. Parent beginWork  → 重新 render
// 2. div beginWork
// 3. Child1 beginWork  → bailout(props 没变)
// 4. Child2 beginWork  → bailout(props 没变)
// 5. 回溯,收集 flags

// Effect List:
// Parent

// Commit 阶段:
// 输出:
// "Parent didUpdate"

// 🔥 结论:只有 Parent 有副作用,只执行 Parent 的生命周期

// ========== useEffect 的执行顺序 ==========

function Parent() {
  useEffect(() => {
    console.log("Parent effect");
    return () => console.log("Parent cleanup");
  });

  return (
    <div>
      <Child1 />
      <Child2 />
    </div>
  );
}

function Child1() {
  useEffect(() => {
    console.log("Child1 effect");
    return () => console.log("Child1 cleanup");
  });

  return <div>Child1</div>;
}

function Child2() {
  useEffect(() => {
    console.log("Child2 effect");
    return () => console.log("Child2 cleanup");
  });

  return <div>Child2</div>;
}

// 首次渲染:
// Commit 阶段(Layout 阶段后,异步执行):
// 输出:
// "Child1 effect"
// "Child2 effect"
// "Parent effect"

// 更新时:
// 输出:
// "Child1 cleanup"
// "Child2 cleanup"
// "Parent cleanup"
// "Child1 effect"
// "Child2 effect"
// "Parent effect"

// 🔥 结论:
// 1. 创建函数:子组件先执行,父组件后执行
// 2. 销毁函数:也是子组件先执行,父组件后执行
// 3. 销毁函数在创建函数之前执行

// ========== useLayoutEffect vs useEffect ==========

function Example() {
  useLayoutEffect(() => {
    console.log("useLayoutEffect");
  });

  useEffect(() => {
    console.log("useEffect");
  });

  return <div>Example</div>;
}

// 执行顺序:
// 1. Commit Mutation 阶段:更新 DOM
// 2. Commit Layout 阶段:useLayoutEffect
// 3. 浏览器绘制
// 4. 异步执行:useEffect

// 输出:
// "useLayoutEffect"
// (浏览器绘制)
// "useEffect"

// 🔥 结论:
// - useLayoutEffect 在浏览器绘制前执行(同步)
// - useEffect 在浏览器绘制后执行(异步)

十一、总结

11.1 Effect List 的核心要点

旧版(React <= 17):

  1. 构建:在 completeUnitOfWork 中手动构建链表
  2. 结构:firstEffect → effect1 → effect2 → ... → lastEffect
  3. 消费:遍历链表,执行副作用
  4. 优点:只访问有副作用的节点
  5. 缺点:额外的内存开销,维护复杂

新版(React >= 18):

  1. 构建:在 bubbleProperties 中收集 subtreeFlags
  2. 结构:每个节点有 flags 和 subtreeFlags
  3. 消费:递归遍历 Fiber 树,根据 flags 判断
  4. 优点:内存小,代码简单,支持并发
  5. 缺点:可能访问一些无副作用的节点(但可以剪枝)

11.2 Commit 阶段的流程

sql 复制代码
Commit 阶段
    ↓
Before Mutation 阶段
    ├─ 调用 getSnapshotBeforeUpdate
    └─ 调度 useEffect(异步)
    ↓
Mutation 阶段
    ├─ 执行 DOM 操作(Placement、Update、Deletion)
    └─ 调用 useLayoutEffect 的销毁函数
    ↓
切换 current 指针
    ↓
Layout 阶段
    ├─ 调用 componentDidMount/componentDidUpdate
    ├─ 调用 useLayoutEffect 的创建函数
    ├─ 更新 ref.current
    └─ 调用 setState 的 callback
    ↓
请求浏览器绘制
    ↓
异步执行 useEffect

11.3 关键记忆点

  • 🔥 副作用在 completeWork 阶段收集(归阶段)
  • 🔥 旧版使用 Effect List 链表 ,新版使用 subtreeFlags
  • 🔥 Commit 阶段分为 三个子阶段(Before Mutation、Mutation、Layout)
  • 🔥 生命周期执行顺序:子组件先执行,父组件后执行
  • 🔥 useLayoutEffect 同步执行 ,useEffect 异步执行
  • 🔥 DOM 操作在 Mutation 阶段执行
相关推荐
用户1456775610373 小时前
文件太大传不了?用它一压,秒变合格尺寸!
前端
用户1456775610373 小时前
再也不用一张张处理了!批量压缩神器来了,快收藏
前端
心.c3 小时前
一套完整的前端“白屏”问题分析与解决方案(性能优化)
前端·javascript·性能优化·html
white-persist3 小时前
【burp手机真机抓包】Burp Suite 在真机(Android and IOS)抓包手机APP + 微信小程序详细教程
android·前端·ios·智能手机·微信小程序·小程序·原型模式
俺会hello我的3 小时前
舒尔特方格开源
前端·javascript·开源
lbh3 小时前
Chrome DevTools 详解(二):Console 面板
前端·javascript·浏览器
ObjectX前端实验室3 小时前
【react18原理探究实践】更新阶段 Render 与 Diff 算法详解
前端·react.js
wxr06164 小时前
部署Spring Boot项目+mysql并允许前端本地访问的步骤
前端·javascript·vue.js·阿里云·vue3·springboot
万邦科技Lafite4 小时前
如何对接API接口?需要用到哪些软件工具?
java·前端·python·api·开放api·电商开放平台