Repository: IduxFE/components (ts)

Issue:
[cdk: subject] add cdk subject
# API

- [x] I have searched the [issues](https://github.com/IduxFE/components/issues) of this repository and believe that this is not a duplicate.

### What problem does this feature solve?
<!-- 这个功能解决了什么问题？ -->
<!-- 请尽可能详尽地说明这个需求的用例和场景。最重要的是：解释清楚是怎样的用户体验需求催生了这个功能上的需求。 -->

自己实现一个类rxjs Subject的事件发布订阅管理

### What does the proposed API look like?
<!-- 你期望的 API 是怎样的？-->
<!-- 描述一下你期望这个新功能的 API 是如何使用的，并提供一些代码示例。请用 Markdown 格式化你的代码片段。 -->

```typescript
declare class Subject<T> {
  public dispatch: (value?: T) => void 
  
  public subscribe: ((value: T) => void) => void
}
```




Pull request description:
feat(cdk: subject): add subject
fix #154

## PR Checklist

Please check if your PR fulfills the following requirements:

- [x] The commit message follows [our guidelines](https://github.com/IduxFE/components/blob/main/docs/contributing.zh.md#commit)
- [x] Tests for the changes have been added/updated or not needed
- [x] Docs and demo have been added/updated or not needed

## PR Type

What kind of change does this PR introduce?

- [ ] Bugfix
- [ ] Feature
- [ ] Component style update
- [ ] Code style update (formatting, local variables)
- [x] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] Application (the showcase website) / infrastructure changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

## What is the new behavior?

## Other information


Patch:
diff --git a/packages/cdk/subject/demo/basic.md b/packages/cdk/subject/demo/basic.md
new file mode 100644
index 00000000..fb6ab2bb
--- /dev/null
+++ b/packages/cdk/subject/demo/basic.md
@@ -0,0 +1,36 @@
+---
+order: 0
+title:
+  zh: 基本使用
+  en: Basic usage
+---
+
+## zh
+
+基本用法。
+
+## en
+
+Basic usage.
+
+## demo
+
+```typescript
+import { Subject } from '@idux/cdk/subject'
+
+const subject1 = new Subject()
+subject1.subscribe(() => {
+  // 订阅
+})
+
+subject1.dispatch() // 发布
+
+/* Subject是一个泛型类 */
+const subject2 = new Subject<number>()
+
+subject2.subscribe(value => {
+  // value会被推导为number
+})
+
+subject2.dispatch(100) // ts类型保护，传参必须为number
+```
diff --git a/packages/cdk/subject/docs/index.zh.md b/packages/cdk/subject/docs/index.zh.md
new file mode 100644
index 00000000..7386bb98
--- /dev/null
+++ b/packages/cdk/subject/docs/index.zh.md
@@ -0,0 +1,35 @@
+---
+category: cdk
+type:
+title: Subject
+subtitle:
+cover:
+---
+
+
+
+## 何时使用
+
+## API
+
+### Subject
+
+#### Params
+
+| 名称 | 说明 | 类型 | 默认值 | 备注 |
+| --- | --- | --- | --- | --- |
+| `T` | 发布参数的类型 | `T` | `void` | 是一个泛型，起发布订阅参数类型保护 |
+| `constructor` | - | `() => void` | - | - |
+| `dispatch` | 发布事件 | `(value: T) => void` | - | - |
+| `subscribe` | 订阅事件 | `(observer: Observer<T>) => Subscription` | - | 订阅事件，利用闭包生成事件id，返回退订事件 |
+| `unsubscribeAll` | 退订所有事件 | `() => void` | - |
+
+````typescript
+interface Observer<T> {
+  (value: T): void
+}
+
+interface Subscription {
+  unsubscribe: () => void
+}
+````
diff --git a/packages/cdk/subject/index.ts b/packages/cdk/subject/index.ts
new file mode 100644
index 00000000..1319b39c
--- /dev/null
+++ b/packages/cdk/subject/index.ts
@@ -0,0 +1,2 @@
+export { Subject } from './src/subject'
+export * from './src/type'
diff --git a/packages/cdk/subject/src/subject.ts b/packages/cdk/subject/src/subject.ts
new file mode 100644
index 00000000..ee8ae18f
--- /dev/null
+++ b/packages/cdk/subject/src/subject.ts
@@ -0,0 +1,30 @@
+import type { Observer, Subscription } from './type'
+
+export class Subject<T = void> {
+  private observers = new Map<symbol, Observer<T>>()
+
+  dispatch(value: T): void {
+    this.observers.forEach(observer => {
+      observer(value)
+    })
+  }
+
+  subscribe(observer: Observer<T>): Subscription {
+    const id = Symbol()
+    this.observers.set(id, observer)
+
+    return this.subscription(id)
+  }
+
+  unsubscribeAll(): void {
+    this.observers.clear()
+  }
+
+  private subscription(id: symbol): Subscription {
+    return {
+      unsubscribe: () => {
+        this.observers.delete(id)
+      },
+    }
+  }
+}
diff --git a/packages/cdk/subject/src/type.ts b/packages/cdk/subject/src/type.ts
new file mode 100644
index 00000000..10875737
--- /dev/null
+++ b/packages/cdk/subject/src/type.ts
@@ -0,0 +1,7 @@
+export interface Observer<T> {
+  (value: T): void
+}
+
+export interface Subscription {
+  unsubscribe: () => void
+}


Test patch:
diff --git a/packages/cdk/subject/__tests__/subject.spec.ts b/packages/cdk/subject/__tests__/subject.spec.ts
new file mode 100644
index 00000000..d0b7bb9b
--- /dev/null
+++ b/packages/cdk/subject/__tests__/subject.spec.ts
@@ -0,0 +1,56 @@
+import { isObject } from '@idux/cdk/utils'
+import { Subject } from '../src/subject'
+
+describe('subject.ts', () => {
+  test('new Subject work', () => {
+    const subject = new Subject()
+    expect(isObject(subject)).toBeTruthy()
+  })
+
+  test('no type subject', () => {
+    const subject = new Subject()
+
+    subject.subscribe(value => {
+      expect(value).toBeUndefined()
+    })
+
+    subject.dispatch()
+    subject.dispatch(undefined)
+  })
+
+  test('string type subject', () => {
+    const subject = new Subject<string>()
+    const strings = ['foo', 'bar']
+
+    subject.subscribe(value => {
+      expect(value).toEqual(strings.shift())
+    })
+
+    const [foo, bar] = strings
+    subject.dispatch(foo)
+    subject.dispatch(bar)
+  })
+
+  test('unsubscribe', () => {
+    const subject = new Subject()
+    const log = jest.spyOn(console, 'log').mockImplementation(() => {})
+
+    const { unsubscribe } = subject.subscribe(() => log)
+    unsubscribe()
+    subject.dispatch()
+
+    expect(log).not.toBeCalled()
+  })
+
+  test('unsubscribeAll', () => {
+    const subject = new Subject()
+    const log = jest.spyOn(console, 'log').mockImplementation(() => {})
+
+    subject.subscribe(() => log)
+
+    subject.subscribe(() => log)
+    subject.unsubscribeAll()
+    subject.dispatch()
+    expect(log).not.toBeCalled()
+  })
+})