深入浅出 Observable:驯服 Web/JS 异步事件流的“瑞士军刀”?

今天,跟大家聊聊一个在 Web 平台“难产”多年,但江湖上早已流传其传说、众多框架和库默默拥抱的家伙——Observable

是不是感觉 addEventListener 用得有点腻歪了?回调地狱、手动移除监听、组合复杂逻辑时的捉襟见肘……这些痛点,就像鞋里的小石子,时不时硌得慌。Observable 提案,就像一位身怀绝技的武林高手,试图用一种更优雅、更“函数式”的姿态,来解决这些前端事件处理的“疑难杂症”。

这篇文章,从它想解决的问题出发,一路扒开它的前世今生、核心概念、实战技巧,最后再一起畅想下它的未来。准备好了吗?发车!

一、Observable 的灵魂拷问——它到底想干啥?

任何技术的出现都不是空穴来风。Observable 想解决的核心痛点,其实就是我们日常与异步事件打交道时的“不爽”。

想想看,我们用 addEventListener 是怎么操作的:

const controller = new AbortController();
const signal = controller.signal;

function handleMouseMove(e) {
  console.log("鼠标移动:", e.clientX, e.clientY);
}

function handleMouseUp(e) {
  console.log("鼠标松开,停止监听移动");
  // 关键:需要手动移除监听
  element.removeEventListener("mousemove", handleMouseMove);
  // 如果还有其他监听,也得一一移除...
  document.removeEventListener("mouseup", handleMouseUp);
  // 或者用 AbortController 批量取消,但还是得手动调用 abort
  // controller.abort();
}

element.addEventListener(
  "mousedown",
  (e) => {
    console.log("鼠标按下,开始监听移动和松开");
    element.addEventListener("mousemove", handleMouseMove, { signal });
    document.addEventListener("mouseup", handleMouseUp, { signal });
  },
  { signal }
);

// 想在某个条件下停止所有监听?
// controller.abort();

这段代码眼熟吧?是不是有内味儿了?

  1. 命令式 (Imperative): 你得一步步告诉浏览器“做什么”(添加监听、移除监听)。
  2. 状态管理复杂: 需要手动管理监听器的添加和移除,尤其是涉及多个事件、需要条件性取消时,很容易遗漏,造成内存泄漏或逻辑错误。
  3. 组合困难: 想实现“当 A 事件发生后,监听 B 事件,直到 C 事件发生”这种逻辑,往往需要嵌套回调,代码可读性和可维护性直线下降。

Observable 的哲学:把事件流当成一等公民

Observable 说:“别那么麻烦了!咱换个思路。” 它借鉴了函数式编程和响应式编程的思想,把随时间发生的、可能多次的事件,看作是一个数据流(Stream)。这个流,就像数组一样,你可以对它进行各种操作(过滤、映射、组合……),而且这些操作是**声明式 (Declarative)**的。

你不再关心具体怎么添加、移除监听器,而是描述你想要什么样的事件流,以及当流中有数据(事件)或者流结束时,你想做什么。

核心优势总结:

看看用 Observable (假设的 .when() API)改写是什么感觉:

// 描述:监听鼠标按下事件,对于每次按下:
element.when("mousedown").subscribe(() => {
  console.log("鼠标按下,开始监听移动和松开");
  // 描述:监听鼠标移动事件,直到鼠标松开事件发生
  element
    .when("mousemove")
    .takeUntil(document.when("mouseup")) // 关键:声明式终止
    .subscribe({
      next: (e) => console.log("鼠标移动:", e.clientX, e.clientY),
      complete: () => console.log("鼠标松开,移动监听自动停止"), // 流完成
    });
});

是不是感觉清爽多了?takeUntil 就像给事件流设了个“截止阀”,一旦 documentmouseup 事件发生,mousemove 事件流就自动停止并清理,无需手动 removeEventListener。这就是 Observable 解决问题的核心思路:用声明式的方式,优雅地管理和组合异步事件流。

二、前世今生——Observable 的“长征路”

了解一项技术,不能只看它光鲜亮丽的 API,还得知道它背后那点“陈芝麻烂谷子”的故事。Observable 的诞生和标准化之路,可谓一波三折,充满了技术大佬们的思考和博弈。

  1. TC39 的萌芽与受挫: 最早,Observable 是在 TC39(ECMAScript 标准委员会)被提出的(大约 2015 年),由 Ben Lesh(RxJS 的核心开发者)等人推动。当时的设想是把它作为 JavaScript 语言的一部分,就像 Promise 一样。但提案在 TC39 卡在了 Stage 1 很长时间。反对的声音认为,Observable 似乎更像是一个“库级”的功能,而不是语言核心必备的;而且它没有引入新的语法,更依赖于 API 设计,这让一些委员觉得它不够“底层”。

  2. 转战 WHATWG: 眼看 TC39 此路不通,大佬们(尤其是 Google 团队的 Ben Lesh)改变策略,尝试将 Observable 作为 Web 平台(DOM)的一部分来标准化(大约 2017 年底,就是 GitHub Issue #544 的开端)。理由是:Web 平台充满了事件(DOM 事件、网络事件等),EventTarget 是事实标准,Observable 与 EventTarget 结合能发挥最大价值,解决 Web 开发者的实际痛点。这似乎更有说服力。

  3. 漫长的讨论与演进: 在 WHATWG 的讨论(Issue #544 里可以看到大量讨论细节)中,API 的形态也几经变化:

    • 最初叫 on(),后来为了避免与现有属性冲突或歧义,改成了 when()
    • 关于 subscribe() 方法的参数形态(函数重载 vs. 对象),Web IDL 的限制引发了讨论(Domenic 的解释)。
    • preventDefault() 的问题被反复提及(Anne van Kesteren 等人提出),因为 Promise 的微任务调度机制可能导致在 then() 中调用 preventDefault() 失效,这促使大家思考同步执行的重要性。
    • AbortController/AbortSignal 的集成,成为取消订阅(unsubscription)的标准方式,这大大增强了它的实用性。
  4. 用户态的繁荣: 标准化进展缓慢,但挡不住人民群众(开发者)的需求啊!以 RxJS 为首的各种 Observable 实现库在社区大行其道,每周几千万甚至上亿的下载量(README 中 Ben Lesh 提到 RxJS 每周 4700 万+下载量),以及众多框架(Angular、Vue (vue-rx)、Svelte、XState 等)对 Observable 的内置支持或良好集成,都证明了其价值和开发者对其的依赖。这反过来也给标准化提供了强大的动力和事实依据——大家都这么用了,标准是不是该跟上了?

  5. WICG 的再出发: 目前,Observable 的标准化工作主要在 WICG(Web Platform Incubator Community Group)进行,由 Dominic Farolino (Google) 等人继续推进。目标是吸取过去的经验教训,整合社区的最佳实践,最终拿出一个浏览器厂商愿意实现、开发者用着顺手的标准 API。

可以说,Observable 的历史,就是一部在语言核心与平台特性之间不断探索、在社区实践与标准化博弈之间不断演进的历史。

三、小试牛刀——EventTarget.when() 初体验

说了这么多背景,是时候上手感受一下了。Observable 提案的核心入口,就是给 EventTarget (我们熟悉的 element, document, window 等的老祖宗) 添加了一个新方法:.when()

.when(eventType, options) 方法返回一个 Observable 对象,这个对象代表了指定类型的事件流。

基础用法:替代 addEventListener

const clicks = element.when("click"); // 返回一个代表点击事件的 Observable

// 订阅这个 Observable,开始监听
const subscription = clicks.subscribe({
  next: (event) => {
    // 每当点击事件发生,next 回调被触发
    console.log("Element clicked!", event.target);
  },
  error: (err) => {
    // 如果 Observable 内部出错(虽然 DOM 事件一般不会)
    console.error("Something went wrong:", err);
  },
  complete: () => {
    // 如果事件流正常结束(DOM 事件流通常不会自然结束)
    console.log("Click stream completed.");
  },
});

// 想停止监听?取消订阅即可
// subscription.abort(); // 假设 subscribe 返回带有 abort 方法的对象,或使用 AbortSignal

链式操作:过滤与映射

这才是 Observable 的魅力所在!假设我们只想处理点击到特定子元素 .foo 的事件,并且只关心点击的坐标:

element
  .when("click")
  .filter((e) => e.target.matches(".foo")) // 过滤:只保留目标匹配 '.foo' 的事件
  .map((e) => ({ x: e.clientX, y: e.clientY })) // 映射:将事件对象转换为坐标对象
  .subscribe({
    next: (point) => {
      // 这里收到的就是坐标对象了
      console.log("Clicked on .foo at:", point);
      handleClickAtPoint(point); // 调用你的处理函数
    },
  });

对比一下用 addEventListener 实现相同逻辑:

element.addEventListener("click", (e) => {
  if (e.target.matches(".foo")) {
    const point = { x: e.clientX, y: e.clientY };
    console.log("Clicked on .foo at:", point);
    handleClickAtPoint(point);
  }
});
// 清理?还得手动 removeEventListener...

是不是高下立判?Observable 的链式调用,把事件的处理流程(过滤、转换)清晰地表达了出来,代码更具声明性。

四、登堂入室——核心 Observable API 与概念

要真正掌握 Observable,光会用 .when() 还不够,得深入理解其核心 API 和运作机制。

1. new Observable(subscribeFn) 构造函数

虽然提案的核心是集成 EventTarget,但 Observable 本身也可以手动创建。构造函数接收一个订阅函数 (subscribe function) subscribeFn 作为参数。

const myObservable = new Observable((subscriber) => {
  // 这个函数在每次调用 myObservable.subscribe() 时执行
  console.log("Subscription started!");
  let i = 0;
  const intervalId = setInterval(() => {
    if (i < 5) {
      subscriber.next(i++); // 发送下一个值
    } else {
      subscriber.complete(); // 发送完成信号,流结束
      clearInterval(intervalId);
    }
  }, 1000);

  // --- 清理逻辑 (Teardown) ---
  // 返回一个函数,或使用 subscriber.addTeardown()
  // 这个函数会在取消订阅或流完成/错误时执行
  const teardown = () => {
    console.log("Subscription teardown: Clearing interval.");
    clearInterval(intervalId);
  };
  subscriber.addTeardown(teardown); // 推荐方式

  // 或者 return teardown; // 老式 API 可能这样
});

console.log("Observable created.");

const subscription = myObservable.subscribe({
  next: (value) => console.log("Received:", value),
  complete: () => console.log("Stream complete!"),
  error: (err) => console.error("Stream error:", err),
});

console.log("Subscribed.");

// 稍后取消订阅
setTimeout(() => {
  console.log("Aborting subscription...");
  subscription.abort(); // 假设返回的对象有 abort 或使用 signal
}, 3500);

关键点:

2. 同步与异步传递 (Synchronous & Asynchronous Delivery)

3. AbortController 与取消订阅

现代 Observable 提案紧密拥抱了 Web 平台的 AbortControllerAbortSignal

// 例子:同步“数据洪流”与 AbortController
const syncObservable = new Observable((subscriber) => {
  let i = 0;
  try {
    while (true) {
      subscriber.next(i++);
      // 检查订阅是否已被外部取消
      if (subscriber.signal.aborted) {
        console.log("Subscription aborted internally, breaking loop.");
        break;
      }
    }
  } finally {
    // 确保清理逻辑被调用
    console.log("Sync observable teardown.");
  }
  // 注意:同步 Observable 通常在循环结束后才 complete/error
  // subscriber.complete(); // 可能不会执行到这里如果被 abort
});

const controller = new AbortController();
syncObservable.subscribe(
  {
    next: (data) => {
      console.log("Sync data:", data);
      if (data > 100) {
        console.log("Data > 100, aborting...");
        controller.abort(); // 从外部取消订阅
      }
    },
    complete: () => console.log("Sync complete."), // 可能不会被调用
    error: (err) => console.error("Sync error:", err),
  },
  { signal: controller.signal }
); // 传入 signal

五、神兵利器——玩转 Observable 操作符

如果说 Observable 对象是内功心法,那操作符 (Operators) 就是各式各样的武功招式。它们是让 Observable 变得强大和灵活的关键。操作符本质上是函数,接收一个 Observable,返回一个新的 Observable,中间对数据流进行处理。

提案中建议内置一些核心且常用的操作符,很多都借鉴自数组方法或 TC39 的 Iterator Helpers 提案,保持了平台 API 的一致性。

分类来看:

  1. 创建型 (Creation):

    • new Observable(): 基础构造器。
    • Observable.from(): 从其他类型转换(后面细说)。
    • EventTarget.when(): 从 DOM 事件创建。
  2. 转换型 (Transformation):

    • map(fn): 对流中的每个值应用函数 fn,发出转换后的值。
      source.map((x) => x * 2); // 把每个值乘以 2
    • filter(fn): 只发出流中满足条件 fn(value)true 的值。
      source.filter((x) => x % 2 === 0); // 只保留偶数
    • flatMap(fn) / switchMap(fn): map 的升级版。fn 需要返回一个 Observable。flatMap 会订阅所有返回的内部 Observable 并合并它们的输出;switchMap 则只关心最新的内部 Observable,当外部 Observable 发出新值时,会取消订阅上一个内部 Observable。常用于处理异步请求(如搜索建议)。
      // 每次输入,发起请求,但只关心最新输入的结果
      inputElement
        .when("input")
        .switchMap((e) =>
          Observable.from(fetch(`/api/search?q=${e.target.value}`))
        )
        .subscribe((results) => updateUI(results));
  3. 过滤/限制型 (Filtering/Limiting):

    • take(n): 只取流中的前 n 个值,然后完成。
    • drop(n): 跳过流中的前 n 个值。
    • takeUntil(notifierObservable): 持续发出值,直到 notifierObservable 发出第一个值或完成/错误,然后完成。这是实现声明式取消的关键!常用于拖拽、游戏序列等。
      // 拖拽示例
      mousedown$
        .flatMap(() => mousemove$.takeUntil(mouseup$))
        .subscribe((pos) => updateElementPosition(pos));
    • filter(fn): (前面已提)
  4. 组合型 (Combination): (提案初期可能不包含,但 userland 常见)

    • merge(obs1, obs2, ...): 合并多个流,任何一个流发值都发出。
    • concat(obs1, obs2, ...): 按顺序连接多个流,等前一个完成后再订阅下一个。
    • zip(obs1, obs2, ...): 将多个流的值按顺序配对发出。
  5. 聚合/终止型 (Aggregation/Termination): 这类操作符通常会消费整个(或部分)流,并返回一个Promise

    • reduce(fn, initialValue): 类似数组的 reduce,对流中所有值进行累加计算,流完成后 Promise resolve 最终结果。
      // 计算鼠标按下期间 Y 坐标最大值 (Example 4 from README)
      const maxY = await element
        .when("mousemove")
        .takeUntil(element.when("mouseup"))
        .map((e) => e.clientY)
        .reduce((max, y) => Math.max(max, y), 0);
    • toArray(): 收集流中所有值到一个数组,流完成后 Promise resolve 这个数组。
    • forEach(fn): 对流中每个值执行 fn,流完成后 Promise resolve undefined
    • first() / last(): 获取第一个/最后一个值,然后完成流,Promise resolve 该值。
    • find(fn) / some(fn) / every(fn): 类似数组方法,找到第一个满足条件的/是否有满足条件的/是否所有都满足条件,Promise resolve 结果。

⚠️ preventDefault 的再次警示

对于上面那些返回 Promise 的聚合/终止型操作符(如 reduce, find, first 等),需要特别注意 preventDefault() 的问题!

// 潜在问题代码
element
  .when("click")
  .first()
  .then((e) => {
    // 这个 then 回调是异步(微任务)执行的
    e.preventDefault(); // 对于脚本触发的 click,这里可能太晚了!
  });

因为 .then() 的回调是异步执行的,如果 click 事件是由脚本(如 element.click())同步触发的,事件的默认行为可能在 then() 回调执行前就已经发生了,导致 preventDefault() 无效。

解决方案: 在 Promise 产生之前,同步地处理 preventDefault

// 方案一:使用 map 同步处理
element.when('click')
  .map(e => {
    e.preventDefault(); // 在 map 中同步调用
    return e; // 仍然传递事件对象
  })
  .first() // first 现在接收的是已经阻止了默认行为的事件
  .then(e => {
    // 做其他事情...
  });

// 方案二:如果提案支持 .do() 或 tap() 操作符 (纯副作用)
element.when('click')
  .do(e => e.preventDefault()) // 同步执行副作用
  .first()
  .then(e => { ... });

// 方案三:如果 first() 支持传入回调 (更特定)
element.when('click')
  .first(e => e.preventDefault()) // 在 first 内部同步处理
  .then(e => { ... });

这是使用 Observable 时需要牢记的一个重要细节。好消息是,这个问题在 RxJS 等库中已存在多年,社区已经习惯并找到了应对方法,所以不必过于恐慌。

六、万物皆可 Observable——Observable.from()

为了让 Observable 能更好地融入现有生态,提案提供了 Observable.from() 静态方法,可以将多种类型的值转换成 Observable:

// 1. 从 Promise 创建
const promise = fetch("/api/data").then((res) => res.json());
const promiseObservable = Observable.from(promise);
promiseObservable.subscribe({
  next: (data) => console.log("Data from promise:", data), // Promise resolve 时触发 next 和 complete
  error: (err) => console.error("Fetch error:", err), // Promise reject 时触发 error
});

// 2. 从数组 (Iterable) 创建
const array = [1, 2, 3];
const arrayObservable = Observable.from(array);
arrayObservable.subscribe({
  next: (value) => console.log("Value from array:", value), // 同步依次发出 1, 2, 3
  complete: () => console.log("Array stream complete."),
});

// 3. 从异步迭代器 (AsyncIterable) 创建 (例如 ReadableStream)
async function* asyncGenerator() {
  yield "a";
  await new Promise((resolve) => setTimeout(resolve, 100));
  yield "b";
}
const asyncObservable = Observable.from(asyncGenerator());
asyncObservable.subscribe({
  next: (value) => console.log("Value from async iterable:", value), // 异步发出 'a', 'b'
  complete: () => console.log("Async stream complete."),
});

// 4. 从另一个 Observable 创建 (幂等)
const source = Observable.from([10, 20]);
const sameObservable = Observable.from(source); // 直接返回 source

这个 from() 方法极大地增强了 Observable 的通用性,让它可以方便地桥接其他异步模式。

七、融会贯通——实战场景演练

理论说了不少,来看几个更接近真实世界的例子,感受 Observable 的威力。

场景 1:WebSocket 消息多路复用 (Multiplexing) (来自官方 README Example 5)

想象一下,一个 WebSocket 连接需要同时处理多种类型的消息(比如不同股票的报价)。我们希望为每种类型的消息创建一个独立的 Observable 流,并且当订阅某个流时自动发送订阅消息给服务器,取消订阅时自动发送取消订阅消息。

const socket = new WebSocket("wss://example.com");

// 通用的多路复用函数
function multiplex({ startMsg, stopMsg, match }) {
  // 等待 socket 连接成功
  const readyToSend$ =
    socket.readyState === WebSocket.OPEN
      ? Observable.from([true]) // 已连接,立即发送
      : socket
          .when("open")
          .map(() => true)
          .take(1); // 等待 open 事件

  return readyToSend$.flatMap(() => {
    console.log("Sending start message:", startMsg);
    socket.send(JSON.stringify(startMsg));

    // 返回一个 Observable,它:
    return socket
      .when("message") // 监听所有消息
      .map((e) => JSON.parse(e.data)) // 解析 JSON 数据
      .filter(match) // 过滤出匹配的消息
      .takeUntil(socket.when("close")) // 当 socket 关闭时停止
      .takeUntil(socket.when("error")) // 当 socket 错误时停止
      .finally(() => {
        // 无论如何结束,都执行清理
        // 清理逻辑:发送停止消息
        if (socket.readyState === WebSocket.OPEN) {
          console.log("Sending stop message:", stopMsg);
          socket.send(JSON.stringify(stopMsg));
        }
      });
  });
}

// 特定股票流的工厂函数
function streamStock(ticker) {
  return multiplex({
    startMsg: { ticker, type: "sub" },
    stopMsg: { ticker, type: "unsub" },
    match: (data) => data.ticker === ticker, // 匹配对应 ticker 的数据
  });
}

// 创建不同股票的流
const googTrades$ = streamStock("GOOG");
const nflxTrades$ = streamStock("NFLX");

// 订阅 GOOG 股票流
const googSubscription = googTrades$.subscribe({ next: updateGoogView });
// 订阅 NFLX 股票流
const nflxSubscription = nflxTrades$.subscribe({ next: updateNflxView });

// 稍后,用户不想看 GOOG 了
googSubscription.abort(); // 取消订阅,会自动触发 finally 发送 unsub 消息

// 如果 socket 意外关闭或出错,所有流也会自动停止并尝试发送 unsub

这个例子展示了 Observable 如何优雅地处理:

场景 2:实现“秘密指令”输入检测 (来自官方 README Example 6)

检测用户是否按顺序输入了一系列特定的按键(比如经典的 Konami Code)。

const konamiCode = [
  "ArrowUp",
  "ArrowUp",
  "ArrowDown",
  "ArrowDown",
  "ArrowLeft",
  "ArrowRight",
  "ArrowLeft",
  "ArrowRight",
  "b",
  "a",
  "Enter",
];

const keydown$ = document.when("keydown").map((e) => e.key);

keydown$
  .bufferCount(konamiCode.length, 1) // 创建一个滑动窗口,每次包含 11 个按键
  // .buffer(keydown$.debounceTime(1000)) // 或者用 buffer + debounceTime 检测连续按键序列
  .filter((keys) => keys.every((key, i) => key === konamiCode[i])) // 检查窗口内容是否匹配
  .subscribe(() => {
    console.log("Konami Code Entered! 🎉");
    // 执行你的彩蛋逻辑...
  });

// RxJS 可能有更直接的操作符如 sequenceEqual 或 window/buffer 组合
// 上面的 bufferCount 是一个简化的思路,实际可能需要更复杂的逻辑
// 比如 RxJS 的:
// keydown$.windowCount(konamiCode.length, 1)
//   .flatMap(window => window.sequenceEqual(Observable.from(konamiCode)))
//   .filter(matches => matches)
//   .subscribe(() => console.log('Konami Code Entered! 🎉'));

(注:原生 Observable 提案初期可能不包含 bufferCountwindowCountsequenceEqual 等高级操作符,这里仅作示例。但这个例子展示了 Observable 在处理序列模式匹配方面的潜力。)

这个例子说明,通过组合操作符,Observable 可以用来识别和响应复杂的事件模式。

八、站在巨人肩上——总结与展望

好了,关于 Observable,我们从理念到实践,聊了不少。现在,让我们站在开发者和提案者的角度,做个总结和展望。

开发者视角:Observable 带来了什么?

  1. 代码更优雅: 声明式的链式调用,让复杂的异步事件处理逻辑(过滤、映射、组合、节流、防抖、取消)变得更清晰、更易读、更易维护。
  2. 解放双手: 自动的资源管理(Teardown 机制),让你告别手动 removeEventListener 的烦恼和潜在的内存泄漏。
  3. 统一模型: 有望提供一个统一的、强大的模型来处理各种异步数据流,不仅仅是 DOM 事件,还包括动画、网络请求、用户输入等。
  4. 性能潜力: 原生实现通常比用户态库有更好的性能,并且可以更好地与浏览器 DevTools 集成,提供更好的调试体验。
  5. 潜在的 Bundle Size 减小: 如果 Observable 成为原生 API,那么像 RxJS 这样的库可以做得更小(只提供原生不包含的操作符),或者开发者可以直接使用原生 API,减少项目依赖体积。
  6. 需要注意的坑: 主要是 Promise-returning 操作符与 preventDefault() 的交互问题,需要养成良好的处理习惯。

提案者视角:道阻且长,行则将至

  1. 漫漫长路: Observable 的标准化之路异常坎坷,反映了在 Web 平台添加新基础原语的复杂性和挑战性(语言 vs. 平台,API 设计细节,厂商协调等)。
  2. 社区力量: 用户态库的巨大成功和广泛应用,是推动其标准化的最有力武器。它证明了开发者确实需要这样的工具。
  3. 平台整合: 将 Observable 与 EventTargetAbortController 等现有平台特性深度整合,是其最终成功的关键。它不是孤立的 API,而是 Web 异步处理生态的一部分。
  4. 未来可期: 随着提案在 WICG 的推进,以及 Chrome 135 已经上架了 Observable API,,我们有理由期待,在不久的将来,或许就能在更多的浏览器中原生使用 Observable 了。

总结

Observable 不是银弹,但它确实为我们处理 Web 异步事件流提供了一套强大而优雅的范式。它鼓励我们用声明式的思维去构建可组合易于管理的事件处理逻辑。虽然它的标准化历程充满波折,但其背后蕴含的响应式编程思想,以及在社区中展现出的强大生命力,都预示着它可能是 Web 开发的下一个重要基石。

希望这篇文章能帮你揭开 Observable 的神秘面纱。但是目前并没有非常靠谱的 observable-polyfill ,但因为这个提案本身就是从社区中践行演化出来的,你可以在官方 README/userland-libraries 找到一些相似的库,提前感受响应式编程的魅力吧!