思维破冰:TypeScript使用的思考

TypeScript

我之前实用TypeScript一直将它作为一种标准的强类型语言看待,甚至有些时候忽略了他有像unknown、any、never等这样在强类型语言到弱类型语言的EscapeHatches。感觉以前的自己对于TypeScript的理解还是太狭隘了。

createSlottedTaskDispatcher设计及启发

今天在实现StackDog中AdvancedTaskLoop时突然发现自己之前对TypeScript太过于循规蹈矩了,而并没有把他看做JavaScript的超集。很多时候都是沿用的OOP一些比较格式化的做法,比如写一个东西必须要设置一个类,对于一个类的操作要封装到一个操作类内部等等。今天在实现createTaskDispatcher API时,突然发现TypeScript中的类型判定更加灵活的用法。这里先解释一下createTaskDispatcher的用途。

createSlottedTaskDispatcher是用于生成slottedTaskDispatcher的一个工厂方法。首先我们需要讲一下TaskDispatcher是什么东西。

TaskDispatcher是我设计的一个AdvancedWorkLoop中起事件分发的一个UnitOfWork工作单元,我将一个Task任务定义为从任务开始到completeWork这段时间内执行的所有UnitOfWork(参考React reconciler),即一个Work中包含多个工作单元,前一个工作单元将会触发后一个工作单元的执行。这样设计是为了StackDog中关于CoerceChain部分的实现和StackDog自身的拓展能力,详细的问题将会在以后的StackDog文章中讲解。这里我将工作单元暂时分为两类,一类为TaskDispatcher,另外一类是TaskExecutor。

TaskDispatcher的一个基本用途就是复用复杂的条件判断,比如如果进行一个空闲空间的判断比较复杂,同时我有两种情况都需要使用这个判断,第一种是我在插入对象到目标位置时,需要判断目标位置是否空闲,第二种是在移动对象是需要判断目标位置是否空闲,可能大部分人觉得只需要封装为一个函数,而后在两个代码位置进行调用就行。但这个操作是一个递归操作,且我需要将每一个这种类型的复杂判断都能够复用,这里能够复用是指在能够像Switch一样在每一个Case下都能从函数外部变更Case下的执行语句,而不需要直接修改这个类型判断的源码。同时这里需要支持WorkLoop,所以我这里就设计了一个TaskDispatcher用于封装一些复杂判断。

我设计的API是这样的,首先有一个createSlotTaskDispatcher函数,这个函数第一个参数接收ConditionList(条件列表),这个条件列表用于存储这个TaskDispatcher的输出有多少种条件,比如上面判空逻辑就应该有两种输出:空闲、非空闲。第二个参数接收一个dispatcher函数,这个函数是一个类型于中间件的一个函数,可以对上下文Context和负载Payload进行操作。第三个参数是一个dispatcher函数,这个函数最为重要,dispatch函数的参数是条件列表中的任意一个条件,如果条件列表是spare和not_spare,那么就可以调用dispatch(“spare”)和dispatch(“not_spare”)两种不同的条件。下面我们来讲这个函数的返回值。

createTaskDispatcher函数的返回值是一个叫SlottedTaskDispatcher类型的对象,这个对象中包含有与ConditionList中条件一样名称的函数,我把这中函数成为Mounter函数,这种函数的作用就是将自定义的回调函数注册进SlottedTaskDispatcher中,当dispatcher函数中调用dispatch(“spare”)或者dispatch(“not_spare”)时,这个被挂载的Mounter函数就会被执行。

上面讲了Mounter函数,但最重要的怎么使用这个SlottedTaskDispatcher没有讲,这里的SlottedTaskDispatcher实际上也是一个函数,当调用这个函数时将返回一个TaskDispatcher类型的函数,这个函数就是我们注入Mounter的Dispatcher,也可以说这个函数就是上面传入createSlotTaskDispatcher的dispatcher函数。当我们将Context Payload传入这个dispatcher中时,其中对dispatch函数的调用就会实际调用到我们的Mounter函数。

这里总结一下,其实我们做的事情就是将条件判断完后进行操作的部分设置成了一个个Slot,而我们可以在外部将我们需要执行的代码注入到Slot中,完成整个条件判断下的执行逻辑。就是React和Vue中Slot。

上述逻辑看起来不是很复杂,但却是我在打破思维定式后完成的设计。如果是没有打破思维定式之前设计,我肯定会将这个TaskDispatcher设计成一个类,然后这些操作都作为类的一个方法,比如 if(“spare”,()=>{//do something here}).if(“not_spare”,()=>{//do something here})。这样虽然也可以,但失去了我希望将UnitWork设计为函数的初衷。然后我就开始思考,为什么我一定要做成类呢?为什么一定要在这种明显OOP不是特别实用的地方遵循OOP呢?我为什么不内部直接用any类型将对象拼装起来,然后用Type Cast转换为我需要的类型呢?

因此我就设计了以下代码:

export type ArrayToDispatch<A> = A extends string[] ? Dispatch<ArrayRetreiver<A>> : never;
export type Dispatcher<C,P> = (ctx: C, payload: P) => void;
export type Dispatch<ActionType> = (action: ActionType) => void;
export type DispatchMounter = (cb:(ctx:Context,payload:Payload)=>void)=>void;
export const Dispatcher = Symbol("dispatcher");
export const Mounter = Symbol("mounter");
export type SlotedDispatch<Actions extends string> = {[key in Actions]:DispatchMounter};
type Context ={};
type Payload ={
  leftSpare:number;
  rightSpare:number;
}
type SplitString<S, Separater extends string = "|", AL = never> = S extends `${infer A}${Separater}${infer Rest}` ? SplitString<Rest, Separater, AL | A> : S | AL;
export function createSlotTaskDispatcher<U extends string,C=Context, P=Payload>(conditionList: U, dispatcher: (ctx: C, payload: P, dispath: Dispatch<SplitString<U>>) => void):SlotedDispatch<SplitString<U>> & { ():Dispatcher<C,P>} {
  let conditions = conditionList.split("|");
  //generate dispatcher
  const dispatcherWrapper:Dispatcher<C,P>=(ctx,payload)=>{
    const dispatch = (action:SplitString<U>)=>{console.log(action);console.log(ret[Mounter]);ret[Mounter][action](ctx,payload);}
    dispatcher(ctx,payload,dispatch);
  }
  let ret:any = ()=>dispatcherWrapper;
  //set dispatch fn
  ret[Dispatcher]=dispatcher;
  ret[Mounter]={};
  conditions.forEach(condition=>{
    ret[condition]=(fn:Parameters<DispatchMounter>[0])=>{
      ret[Mounter][condition]=fn;
    };
  })
  return ret as SlotedDispatch<SplitString<U>> & { ():Dispatcher<C,P>};
}

可以看到,我在createSlotTaskDispatcher函数中,实际上是使用了一个any对象将整个返回值给拼装起来返回。这里使用两个Symbol类型用于控制用户访问。可以看到我先定义了将Dispatcher的函数然后将这个函数作为一个匿名函数的返回值后赋值给了any类型对象ret,然后将dispatcher参数装载到ret上,最后使用循环将不同条件下的condition名设置为ret上的一个函数,函数内部是Mounter的赋值语句。这里的逻辑也不是很复杂,只需要抓住最后返回值类型就可以理清楚,这里返回值转化为了一个函数 & SloteedDispatch的类型对象,最终让我们能够通过智能提示提示我们这个对象内部可以使用API。

下面是对该API的一个简单使用:

type Context ={};
type Payload ={
  leftSpare:number;
  rightSpare:number;
}
//=====//
let sloted = createSlotTaskDispatcher(
      "spare|not_spare",
      (ctx, payload, dispatch) => {
        if (payload.leftSpare === 0 && payload.rightSpare === 0) {
          dispatch("not_spare");
        }
        dispatch("spare");
      }
    );
    sloted.spare((ctx, payload) => {
      console.log("in spare");
    });
    sloted.not_spare(() => {
      console.log("in not_spare");
    });
    const dispatcher = sloted();

    dispatcher({}, { leftSpare: 100, rightSpare: 100 });

以上代码是这个API的草稿版本,实际上这里传入的可能是一个TaskExecutor或者另外一个Dispatcher,因此这里的API还会修改,以后应该会作为独立的一个设计模型写一篇文章。

在之前我一直不是很理解一些库中是怎样的实现的TypeCheck,在今天这样实践了一次后我感觉给我打开了TypeCheck的一扇新窗。以后应该对这类类型检查的实现会更加敏感和更加高的理解度。

其实在之前低代码服务器设计与实现的文章中我就已经有一些OOP在框架实现情况下的不适用感觉,在一些更加灵活的底层代码中,还是使用上述这样的思考模式更加有利于复杂API的构建和实现。


v1.0 wep 添加基本内容

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注