> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 事件

> Aftersell Cart SDK 的所有事件：每个事件的触发时机、传递的内容、适用场景，以及导致无限循环的错误。

事件让你可以在购物车中**发生某事时**运行代码。它们位于 `window.aftersell.cart.events` 下。

订阅是一个设置类调用，所以可以安全地放在脚本顶部，无需等待 `ready()`。

<div id="available-events">
  ## 可用事件
</div>

| 事件                                            | 载荷                                                    | 触发时机               |
| --------------------------------------------- | ----------------------------------------------------- | ------------------ |
| [`cart_loaded`](#cart_loaded)                 | [`AftersellCart`](/zh/aftersell/cart/sdk-cart-object) | 购物车加载时，每页一次。       |
| [`cart_updated`](#cart_updated)               | [`AftersellCart`](/zh/aftersell/cart/sdk-cart-object) | 首次加载之后，购物车内容发生变化时。 |
| [`item_added`](#item_added)                   | `{ item }`                                            | 购物车中出现新的一行时。       |
| [`item_removed`](#item_removed)               | `{ item }`                                            | 某行从购物车中消失时。        |
| [`cart_opened`](#cart_opened-and-cart_closed) | 无                                                     | 抽屉打开时。             |
| [`cart_closed`](#cart_opened-and-cart_closed) | 无                                                     | 抽屉关闭时。             |
| [`checkout`](#checkout)                       | 无                                                     | 结账按钮被点击时。          |

<div id="subscribing">
  ## 订阅
</div>

`events.on(event, handler)` 注册一个处理函数并**返回一个用于取消订阅的函数**：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const off = window.aftersell.cart.events.on('cart_updated', (state) => {
  console.log('Cart total is now', state.totalPrice);
});

// later, to stop listening:
off();
```

* `events.once(event, handler)`：触发一次后自行取消订阅。
* `events.off(event, handler)`：移除特定的处理函数。

抛出错误的处理函数会被隔离并记录到控制台；其他处理函数仍会运行。

***

<div id="the-two-rules">
  ## 两条规则
</div>

几乎所有事件相关的 bug 都可以追溯到这两条之一。

<div id="dont-change-the-cart-from-cart_updated-without-a-guard">
  ### 不要在没有防护的情况下从 `cart_updated` 更改购物车
</div>

在 `cart_updated` 处理函数内部更改购物车会再次触发 `cart_updated`。如果那个处理函数又更改了购物车，你就有了一个无限循环。购物者会看着购物车反复抖动，而页面则疯狂请求 Shopify。

<Warning>
  \*\*永远不要从 `cart_updated` 或 `cart_loaded` 中无条件地调用操作。\*\*用一个针对你即将创建的状态的检查来防护它，使第二次执行什么都不做。
</Warning>

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// ❌ Loops forever: every add triggers an update, which triggers another add.
window.aftersell.cart.events.on('cart_updated', (state) => {
  window.aftersell.cart.actions.addItem(GIFT_VARIANT_ID, 1);
});

// ✅ Guarded: once the gift is present, the condition is false and it stops.
window.aftersell.cart.events.on('cart_updated', (state) => {
  const hasGift = state.items.some((line) => line.variantId === GIFT_VARIANT_ID);
  if (state.totalPrice >= 5000 && !hasGift) {
    window.aftersell.cart.actions.addItem(GIFT_VARIANT_ID, 1);
  }
});
```

购物车确实给了你一道安全网：产生**完全相同**购物车的更新不会发出任何事件，所以没有任何变化的重新获取不会重启循环。这能保护你免受意外的空操作循环。但它**不能**保护你免受每次都真正更改购物车的处理函数的影响。

<div id="treat-the-payload-as-read-only">
  ### 将载荷视为只读
</div>

同一事件的所有处理函数接收的是*同一个*对象。修改它会改变你之后的处理函数看到的内容，包括商店中其他应用的处理函数。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// ❌ Corrupts the payload for every later handler.
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items = state.items.filter((line) => line.finalLinePrice > 0);
});

// ✅ Copy first.
window.aftersell.cart.events.on('cart_updated', (state) => {
  const paidItems = state.items.filter((line) => line.finalLinePrice > 0);
});
```

要真正更改购物车，使用[操作](/zh/aftersell/cart/sdk-actions)。要更改行的渲染方式，使用 [`registerLineTransform`](/zh/aftersell/cart/sdk-hooks#registerlinetransform)。

***

<div id="cart_loaded">
  ## cart\_loaded
</div>

在购物车于页面上首次加载时触发**一次**。载荷是完整的[购物车对象](/zh/aftersell/cart/sdk-cart-object)。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_loaded', (state) => {
  console.log('Page loaded with', state.itemCount, 'items');
});
```

\*\*适用场景：\*\*任何需要针对购物车初始状态运行的逻辑，例如协调免费赠品、初始化小组件，或在页面加载时向分析工具上报购物车内容。

\*\*`cart_loaded` 会向迟来的订阅者重放。\*\*如果你在购物车已经加载后才订阅，你的处理函数会立即以当前购物车被调用。订阅顺序从不重要，所以你不必担心你的脚本是否抢在了购物车之前。

<Tip>
  既要在页面加载时正确、又要在之后每次变化时正确的逻辑，应该用同一个函数**同时**订阅 `cart_loaded` 和 `cart_updated`。这是"让 X 与购物车保持同步"的标准模式。
</Tip>

<div id="cart_updated">
  ## cart\_updated
</div>

在首次加载**之后**，购物车内容每次变化时触发，无论变化来自抽屉、你自己的操作、主题还是其他应用。载荷是完整的[购物车对象](/zh/aftersell/cart/sdk-cart-object)。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_updated', (state) => {
  document.querySelector('#my-total').textContent =
    window.aftersell.cart.actions.formatMoney(state.totalPrice);
});
```

\*\*适用场景：\*\*让购物车之外的东西保持同步，例如自定义总额、进度条、页眉徽章，或每次变化时的分析事件。

产生完全相同购物车的更新不会发出任何事件。重新打开抽屉、切换回标签页，或返回相同内容的重新获取都不会触发它。

<Warning>
  在这里调用操作之前，请重读[两条规则](#the-two-rules)。
</Warning>

<div id="item_added">
  ## item\_added
</div>

在购物车中出现**新的一行**时触发。载荷是 `{ item }`，其中 `item` 是[购物车行](/zh/aftersell/cart/sdk-cart-object#cart-lines)。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('item_added', (payload) => {
  myAnalytics.track('Added to cart', {
    id: payload.item.variantId,
    title: payload.item.title,
    quantity: payload.item.quantity,
  });
});
```

\*\*适用场景：\*\*在第三方分析工具中追踪加入购物车。这是 SDK 最常见的用途。参阅[追踪加入购物车](/zh/aftersell/cart/sdk-use-case-analytics)。

关于它的推导方式，有两点需要知道：

<Warning>
  \*\*数量变化不算添加。\*\*购物车通过对*行*而不是数量进行差异比较来判断添加和移除。购物者把某行从 1 增加到 3 会触发 `cart_updated`，而不是 `item_added`。如果你也需要捕获数量增加，请在 `cart_updated` 处理函数中与之前的状态进行比较。
</Warning>

它也不会为页面加载时已经在购物车中的商品触发；那些通过 `cart_loaded` 到达。一次性添加多个不同产品会为每一行触发一次该事件。

<div id="item_removed">
  ## item\_removed
</div>

在某行从购物车中消失时触发。载荷是 `{ item }`，即该行消失前一刻的样子，所以你仍然可以读取它的 `key`、`variantId` 和 `title`。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('item_removed', (payload) => {
  console.log('Removed', payload.item.title);
});
```

\*\*适用场景：\*\*撤销你在添加时做的事，例如清除标志、重新显示购物者拒绝过的优惠，或向分析工具上报移除。

与 `item_added` 有同样的注意事项：数量降低但未归零不算移除。

<div id="cart_opened-and-cart_closed">
  ## cart\_opened 和 cart\_closed
</div>

在抽屉打开和关闭时触发。没有载荷。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_opened', () => {
  myAnalytics.track('Cart viewed');
});

window.aftersell.cart.events.on('cart_closed', () => {
  document.body.classList.remove('cart-is-open');
});
```

\*\*适用场景：\*\*浏览追踪、暂停抽屉后面的视频或轮播、切换页面上的类名。

两者都不会在页面初始加载时触发，只在实际打开或关闭时触发。

<div id="checkout">
  ## checkout
</div>

在购物者点击结账按钮时、浏览器跳转之前触发。没有载荷。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('checkout', () => {
  myAnalytics.track('Checkout started');
});
```

\*\*适用场景：\*\*结账意向追踪。

<Warning>
  \*\*你无法从这个处理函数中取消结账。\*\*该事件是一个通知，而不是一道闸门；无论你的代码做什么，跳转都会发生。让处理函数保持快速和同步：`await` 或慢速网络调用可能在页面卸载前无法完成。任何需要可靠发送的内容请使用 [`navigator.sendBeacon`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon)。
</Warning>

***

<div id="listening-from-outside-the-sdk">
  ## 从 SDK 外部监听
</div>

每个事件也会作为 DOM `CustomEvent` 在 `window` 上派发，所以你可以在不接触 `window.aftersell.cart` 的情况下监听。这在主题文件、第三方应用或独立于购物车加载的脚本中很有用。

| 总线事件           | DOM 事件                        |
| -------------- | ----------------------------- |
| `cart_loaded`  | `aftersell:cart:cart-loaded`  |
| `cart_updated` | `aftersell:cart:cart-updated` |
| `item_added`   | `aftersell:cart:item-added`   |
| `item_removed` | `aftersell:cart:item-removed` |
| `cart_opened`  | `aftersell:cart:cart-opened`  |
| `cart_closed`  | `aftersell:cart:cart-closed`  |
| `checkout`     | `aftersell:cart:checkout`     |

注意命名：总线使用 `snake_case`，DOM 事件在 `aftersell:cart:` 前缀后使用 `kebab-case`。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.addEventListener('aftersell:cart:cart-updated', (event) => {
  console.log('Cart total is now', event.detail.totalPrice);
});
```

载荷通过 `event.detail` 到达，与[购物车对象](/zh/aftersell/cart/sdk-cart-object)一致。事件在 `window` 上派发，所以页面上任何位置的监听器都能收到。购物车渲染在 shadow root 中，但 shadow 边界从不在事件的传播路径中。每次派发都会克隆载荷，所以修改 `event.detail` 的监听器不会影响其他任何人，抛出错误的监听器也不会干扰 SDK。

<Warning>
  \*\*`cart-loaded` 不会在 DOM 上重放。\*\*总线会向迟来的订阅者重放 `cart_loaded`，但那条路径绕过了 DOM 派发，所以在购物车已加载后注册的 `window.addEventListener('aftersell:cart:cart-loaded')` 永远不会触发。如果你的脚本加载顺序无法保证，请使用会重放的 `window.aftersell.cart.events.on('cart_loaded', …)`，或者同时监听 `aftersell:cart:cart-updated`。
</Warning>

<div id="shopify-standard-cart-events">
  ### Shopify 标准购物车事件
</div>

另外，购物车在每次更改购物车时都会在 `document` 上发布 Shopify 的[标准购物车事件](https://shopify.dev/docs/storefronts/themes/best-practices/standard-events)，这样主题代码和其他应用就可以像响应主题的变更一样响应 Aftersell 的变更：

| 事件                             | 事件实例上的载荷                                                                       |
| ------------------------------ | ------------------------------------------------------------------------------ |
| `shopify:cart:lines-update`    | `action: 'add' \| 'update' \| 'remove'`、`context: 'cart' \| 'product'`、`lines` |
| `shopify:cart:note-update`     | `context: 'cart'`、`note`                                                       |
| `shopify:cart:discount-update` | `discountCodes: [{ code }]`                                                    |

<Warning>
  **载荷不在 `event.detail` 上。**`detail` 只携带 `{ source: 'aftersell' }`——购物车用来忽略自己的事件以避免循环的标签。上表中的所有内容都直接赋值到事件对象上，所以要读取 `event.action`，而不是 `event.detail.action`。
</Warning>

每个事件还携带一个 `promise`，Aftersell 会在底层写入落定时将其 settle，符合 Shopify 的标准——await 它，不要去 resolve 它。这些事件在 `document` 上派发并会冒泡，所以 `window` 上的监听器也能收到。

<div id="where-to-go-next">
  ## 后续阅读
</div>

* **[购物车对象](/zh/aftersell/cart/sdk-cart-object)**：上述载荷的完整结构。
* **[操作](/zh/aftersell/cart/sdk-actions)**：如何从处理函数中更改购物车。
* **[Hooks](/zh/aftersell/cart/sdk-hooks)**：用于更改购物车的渲染方式，而不是响应它。
* **[使用案例](/zh/aftersell/cart/sdk-use-cases)**：分析追踪、免费赠品和其他完整示例。
