我们都知道控制反转这个概念,即将控制权交给外部调用者。
在前端的使用场景是怎么样的呢?
我们来举个例子,正向的使用一个变量是非常方便的,比如我有一个数组对象,要把他渲染出来:
const arr = ["apple", "banana", "orange"];
const App = () => {
return (
<div>
{arr.map(item => <div>{item}</div>)}
</div>
)
}
在我们的代码中,我们是可以直接感知到数组的内容的,这是我们正向的编程思维,即我们通过数据的引用链路是可以看到所有的我们需要知道的细节的。
但是假如说我们需要根据条件动态的添加不同的内容,要怎么实现呢?
一种还是正向的思维,列举所有的可能性,如:
const arr = ["apple", "banana", "orange"];
if(flag1) {
arr.push('pear')
}
if(flag2) {
arr.push('watermelon')
}
//...
另一种方式则是反向的思维,在运行时动态的加载.
const arr = ["apple", "banana", "orange"];
export function appendFruit(fruit: string) {
arr.push(fruit)
}
appendFruit('pear')
appendFruit('watermelon')
可以看到我用了三个代码块,意味着他们可能是在不同的文件中,甚至是在不同的插件中。根据外部的一些实际使用情况来在运行时中动态的加载。
与第一种方式的区别在于他们解耦了。通过规定一个链接协议: appendFruit 将 arr 与 fruit 串联起来。
对于 arr 来说,他是不会知道除了自身默认的三个值外还有可能会存在那些内容,因为从他的角度来看是没有直接引入所有的可能性。
抽象的,我将其称之为注册机模式。即使用者暴露一个注册方法,在外部通过注册的方式将想要实现的内容插入进去。
我们可以写出这样的工具函数:
export function buildRegList<T>(): [T[], (item: T) => void] {
const list: T[] = [];
const reg = (item: T) => {
list.push(item);
};
return [list, reg];
}
使用方式也非常简单:
export const [pluginGroupPanel, regGroupPanel] = buildRegList<PluginGroupPanel>();
其中 pluginGroupPanel 是提供给使用者使用的,使用者并不需要关心里面的内容,只需要根据约定的协议渲染出来即可
regGroupPanel 是提供给外部的,外部不需要了解传入的数据是如何渲染的,只需要根据协议与需要将内容注册即可。
想要实现对象类型的注册函数也很简单,只需稍微调整一下即可:
export function buildRegMap<T>(): [
Record<string, T>,
(name: string, item: T) => void
] {
const mapping: Record<string, T> = {};
const reg = (name: string, item: T) => {
if (mapping[name]) {
console.warn('[buildRegMap] Duplicate Register:', name);
}
mapping[name] = item;
};
return [mapping, reg];
}
在 Tailchat 中大量使用了注册机模式,将插件的实现注入到核心代码中,而无需修改核心代码。这样相同的逻辑会因用户加载了不同的插件产生不同的效果。
本文实战用法见: github.com/msgbyte/tai…
我会不定期的从 Tailchat 的源码中拆出一些实用的实战小技巧分享给大家,关注我,并给 Tailchat 点点 star。我将带你以实战理解算法的妙用。