依赖注入是一种设计模式,核心思想是:不要在类内部创建依赖,而是通过外部传入,这样可以减少耦合、提高测试性和灵活性。
不使用依赖注入的例子
class EmailService {
send(email: string, message: string) {
console.log(`发送邮件到 ${email}: ${message}`);
}
}
class UserController {
private emailService = new EmailService(); // 直接创建,耦合很强
notifyUser(email: string) {
this.emailService.send(email, "欢迎加入!");
}
}
使用依赖注入的例子
class EmailService {
send(email: string, message: string) {
console.log(`发送邮件到 ${email}: ${message}`);
}
}
class UserController {
constructor(private emailService: EmailService) {}
notifyUser(email: string) {
this.emailService.send(email, "欢迎加入!");
}
}
// 使用方式
const emailService = new EmailService();
const userController = new UserController(emailService);
- UserController 不再依赖于具体实现,易于替换或测试。
- 更符合“单一职责”和“模块化”的思路。
依赖倒置原则 > 高层模块(业务逻辑)不应该依赖低层模块(工具类),二者都应该依赖于抽象。抽象不应该依赖细节,细节应该依赖抽象。
// 抽象接口
interface MessageService {
send(to: string, message: string): void;
}
// 低层模块实现
class EmailService implements MessageService {
send(to: string, message: string): void {
console.log(`发送邮件给 ${to}: ${message}`);
}
}
// 高层模块
class UserController {
constructor(private messageService: MessageService) {}
notifyUser(email: string) {
this.messageService.send(email, "欢迎!");
}
}
// 使用
const service = new EmailService();
const controller = new UserController(service);
- 高层模块(UserController)只依赖接口(MessageService),与具体实现(EmailService)无关。
- 更容易扩展,比如换成短信服务 SMSService,无需改动 UserController。
实例
export interface RendererOptions<HostNode = RendererNode> {
setElementText(node: HostNode, text: string): void
}
export interface RendererNode {
[key: string]: any
}
export interface RendererElement extends RendererNode {}
export type RootRenderFunction<HostElement = RendererElement> = (
message: string,
container: HostElement,
) => void
export function createRenderer(options: RendererOptions) {
const { setElementText: hostSetElementText } = options
const render: RootRenderFunction = (message, container) => {
hostSetElementText(container, message)
}
return { render }
}
createRenderer
接收了setElementText
并以setElementText
实现并返回了 render
(这里render
是操作DOM
相关的操作), render
接受参数并调用 传入进来的setElementText
,这样实际的操作就与creatRenderer
无关。
createAppAPI
接收了 render
,并在返回的对象的方法中使用了render
,这样顺序就是 app(createApp)
-> render(createRenderer)
-> createRenderer
的参数
GPT解析实例
runtime-dom/index.ts ↓ createRenderer(nodeOps) ↓ render 函数(通过注入 nodeOps 的 setElementText 实现) ↓ createAppAPI(render) ↓ createApp().mount(selector) ↓ rootComponent.render() → render(message, container)
DI(依赖注入)
createRenderer(options) 不自己创建 DOM 操作函数,而是通过参数注入 setElementText。也就是说:渲染逻辑依赖于外部注入的底层实现(DOM 还是 Canvas,甚至是 SSR 都无所谓,只要实现同样的接口就行)。
createRenderer(nodeOps)
DIP(依赖倒置原则)
这体现在以下几点:
- createRenderer 并不依赖于 DOM 或具体的实现细节,而是依赖于抽象接口 RendererOptions。
- createAppAPI 依赖的也是抽象的 render 函数,不关心具体渲染方式。
- 高层模块(如 createAppAPI)依赖的是抽象接口,而不是底层细节。 *
🔧 也就是说:
高层模块 createAppAPI 和底层实现 nodeOps.setElementText 都依赖于 RendererOptions 这个抽象。
这就是典型的 依赖倒置。
✅ 这种设计的好处
- 解耦:逻辑与具体实现完全分离,可以轻松替换渲染逻辑(例如服务端渲染/Canvas 渲染/WebGL 渲染)。
- 扩展性强:想换底层实现,只要实现同样接口就行。
- 测试友好:可以 mock RendererOptions 来测试 createRenderer 的行为。
- 灵活组合:上层代码无需修改即可适配多种运行环境(DOM、Node.js、Native 等)。
✅ 如果要总结一句话:
这套设计用 createRenderer 做了渲染层的抽象,并通过依赖注入(DI)和依赖倒置原则(DIP)将 DOM 操作从业务逻辑中解耦出来,从而构建出一个高度可扩展和可复用的 UI 渲染引擎。
Q & A
Q: 依赖倒置 就是尽可能把依赖抽离到更下一层? A: 依赖倒置不是“放到底层”,而是“依赖抽象,而不是依赖具体实现”
传统的设计是这样的: 高层模块(业务逻辑) ↓ 依赖 低层模块(数据库/文件系统/DOM/网络请求等) 高层模块直接依赖底层模块,一旦底层改变,高层也要改,耦合强,灵活性差。
而“依赖倒置”后: 高层模块 → 依赖接口/抽象 ← 底层模块实现接口
🔁 这就是“倒置”:原本是高层依赖低层,现在是两者都依赖抽象。
来源: https://book.chibivue.land/10-minimum-example/015-package-architecture.html#di-and-dip