zone.js由入门到放弃之五——NgZone & ApplicationRef源码分析

啸达同学刚写zone.js系列就说过,NgZone影响着Angular中的变更检测,历时一个多月的笔耕不辍,终于到了他初次下笔时的目的地~

zone.js系列

初见NgZone

其实在上一篇文章中,大家已经初步窥探过NgZone的芳容了。而且我们也知道了,在NgZone中维护了OuterZone和InnerZone两个Zone。今天的这篇文章,我们主要分析一下InnerZone,并看一下InnerZone是如何跟Angular的变更检测联系到一起的。

1_change_1679915818661.png

InnerZone四方法

NgZone中InnerZone的创建是通过forkInnerZoneWithAngularBehavior完成的,创建过程的简化版如下,其中又能看到很多熟悉的勾子函数。这里简单复习一下这几个勾子的意义:

  • onInvokeTask:zone.js会在初始化的时候将异步方法都Pathc成ZoneTask,从而跟踪异步任务的执行情况的。onInvokeTask就是其中的一个勾子函数,它会在异步任务执行回调的时候触发。
  • onInvokeonInvoke会在我们手动执行zone.run()的时候执行。
  • onHasTask:是针对整个任务队列状态改变的监听,当检测任务队列中有任务进入、或是有任务执行完出队列的时候会被执行。
  • onHandleError:当有异常抛出时被执行

InnerZone对异步任务的控制精华基本上就全部浓缩在这几个勾子函数中了,与此同时,为了更好地配合对异步任务的跟踪,NgZone中还定义了很多状态监控字段。只有理清这些字段的含义才能继续往下深入代码。

不熟悉zone.js原理的可以回看一下zone.js由入门到放弃之一和zone.js由入门到放弃之二(链接见文首)

传送门

function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
  zone._inner = zone._inner.fork({
    name: 'angular',
    properties: <any>{'isAngularZone': true},
    onInvokeTask: (...): any => {
      ...
    },
    onInvoke: (...): any => {
      ...
    },
    onHasTask: (...): any => {
      ...
    },
    onHandleError: (...): any => {
      ...
    },
  });
}

InnerZone五状态

接下来这几个状态属性会贯穿在后面的源码分析的全部过程中,我们也会通过对这几个状态的跟踪了解一下InnerZone事件跟踪的原理。

  • hasPendingMacrotasks: boolean 队列中是否有待执行的宏任务
  • hasPendingMicrotasks: boolean 队列中是否有待执行的微任务
  • _nesting: number 队列中待执行任务的个数
  • isStable: boolean 当任务队列中既没有待执行的宏任务,也没有待执行的微任务时,isStable为ture,表示当前是个稳定的状态。反之则代表非稳定状态。
  • lastRequestAnimationFrameId: number 这个状态有些特别,它是一个延时器,后面会展开解释。

代码走读

前面在介绍zone.js的时候我们说过,zone.js把异步任务分为MacroTask、MicroTask和Event三种。今天我们就分别把这三种任务都按流程分析一遍。从难易程度上看,MacroTask最简单,Event相对最复杂。接下来,我们就按照这个顺序讲解。

2_task_1679915843162.jpg

MacroTask

之前在zone.js由入门到放弃之三中,详细介绍过zone.js对setTimeout的Patch过程,如果不了解具体过程的强烈建议先浏览一下那篇文章。

这一次,我们还是通过个setTimeout事件来跟踪NgZone的处理过程,测试代码很简单,如下所示。

export class AppComponent implements OnInit {
  title = 'ngzone-process';

  ngOnInit(): void {
    setTimeout(() => {
      console.log('[setTimeout] run in next 5s');
    }, 5000);
  }

  ngDoCheck() {
    console.log('rendering...');
  }

}

因为zone.js可以感知到任务队列的变化情况,所以当setTimeout执行时,它可以知道当前有一个宏任务来了,同时会触发onHasTask勾子。

onHasTask

onHasTask"检测"到有宏任务到来时,会把hasPendingMacrotasks设置为true。

传送门

onHasTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
  delegate.hasTask(target, hasTaskState);
  if (current === target) {
    // ...
    } else if (hasTaskState.change == 'macroTask') {
      zone.hasPendingMacrotasks = hasTaskState.macroTask;
    }
  }
},

此时,NgZone中的几个状态值大概是这个样子的,hasPendingMacrotasks变为true,表示当前有一个待执行的MacroTask。

接下来,zone.js会通过调用scheduleFn,并把封装后的回调函数放在Timer队列中等待时钟到达。

hasPendingMacrotaskshasPendingMicrotasks_nestingisStablelastRequestAnimationFrameId
truefalse0true-1
onInvokeTask

当时钟到达以后,事件循环会把封装后的回调函数放在任务队列中等待执行。当执行到回调时,回调会触发task.invoke函数,接下来就会唤醒onInvokeTask勾子函数。

传送门

onInvokeTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
 applyArgs: any): any => {
  try {
    onEnter(zone);
    // 执行真正的回调 👇
    return delegate.invokeTask(target, task, applyThis, applyArgs);
  } finally {
    // ...
    onLeave(zone);
  }
},

delegate.invokeTask(target, task, applyThis, applyArgs);是用来调用真正的回调函数的。除了这行,我们可以看到在回调之前先后分别还各有一个方法:onEnteronLeave

onEnter

onEnter执行过程中,_nesting会自增,表示了当前新增一个待执行任务。当有任务要执行时,之前的稳定状态会被打破,同时触发一个onUnstable事件。这个onUnstable事件被ApplicationRef订阅,ApplicationRef会根据这个事件同步修改它自身的稳定状态(ApplicationRef的代码后面讲解)。

传送门

function onEnter(zone: NgZonePrivate) {
  zone._nesting++;
  if (zone.isStable) {
    zone.isStable = false;
    zone.onUnstable.emit(null);
  }
}
hasPendingMacrotaskshasPendingMicrotasks_nestingisStablelastRequestAnimationFrameId
truefalse1false-1
onLeave

onLeave函数执行的时候,说明MarcoTask的回调已经执行完毕,_nesting会执行一次自减操作。接下来又执行了checkStable函数。

传送门

function onLeave(zone: NgZonePrivate) {
  zone._nesting--;
  checkStable(zone);
}

checkStable函数非常关键!每当执行到checkStable的时候,都是变更检测执行的关键。以至于这个函数的每一行都值得拿出来讲一下,我在代码中标记了序号,这样方便后面走读代码。

  1. checkStable既然是判断是否进行变更检测关键,那么1标识的if子句就是判断的关键。代码的意思大概就是,只有确保当前没有任何待执行的任务,同时当前状态为不稳定状态的时候才需要触发变更检测。
  2. 代码2标识了一个成对的_nesting自增、自减操作。这里这么做的原因是代码3这里抛出了事件,对该事件的订阅实际上也是一个异步任务。所以这里通过_nesting的自增、自减操作说明这里是有一个异步任务的。
  3. 代码3就是变更检测的关键了,AppliactionRef会订阅onMicrotaskEmpty事件,每当onMicrotaskEmpty触发后,AppliactionRef就会执行一次变更检测。
  4. 代码4这里大家可能会有疑问,为什么在这里还要对hasPendingMicrotasks进行一次判断?这是因为在代码3这里,对onMicrotaskEmpty的订阅者有可能会在订阅回调中再执行一些异步任务,就像下面这样。此时,并不能保证在checkStable的过程中,不会有新的任务进入到待执行队列。所以这里,又对hasPendingMicrotasks的状态做了一次判断。确保在状态变为稳定之前,任务队列中不存在任务微任务。
zone.onMicrotaskEmpty.subscribe(() => {
    Promise.resolve(0).then(console.log);
});
  1. 代码5是对外触发一个状态稳定的事件,这个事件跟OnEnter函数中那个onUnstable相对。但是你可能会好奇,这里为什么要在runOutsideAngular中执行。我这里解释下,仅代表个人见解。onStableonMicrotaskEmpty存在一样的问题,因为都是可观察对象,所以存在订阅者在回调继续执行异步任务的问题。如果在onStable的订阅中执行异步任务,那NgZone的状态马上有会变成非稳定的,这将会陷入一个无限的死循环中,NgZone会在稳定和不稳定状态之间来回切换,永不停止。所以这里使用runOutsideAngular,让zone.js放弃对这里的代码进行跟踪。这样,根据上一讲我们学过的内容,runOutsideAngular中执行异步不会触发变更检测,当然也不会触发NgZone的状态变化。
  2. 改变zone的状态为稳定。

传送门

function checkStable(zone: NgZonePrivate) {
  // 👇 1
  if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) {
    try {
      // 👇 2
      zone._nesting++;
      // 👇 3
      zone.onMicrotaskEmpty.emit(null);
    } finally {
      // 👇 2
      zone._nesting--;
      // 👇 4
      if (!zone.hasPendingMicrotasks) {
        try {
          // 👇 5
          zone.runOutsideAngular(() => zone.onStable.emit(null));
        } finally {
          // 👇 6
          zone.isStable = true;
        }
      }
    }
  }
}

这里我多补充一点知识,我之前看到这里的代码的时候也觉得有点绕,所以我在这里做了大量的测试。结果发现,如果在onStable的订阅回调中再使用zone.run执行异步任务的时候就会造成一个无限的死循环。这里是我的最小实现仓,够胆的可以试试,你的浏览器会在瞬间崩溃。当然,我也给官方提了issue,原作者也证实了这的确是个问题,感兴趣的可以跟踪一下,持续关注。

截止到这里,我们再看一下NgZone的几个状态指标。此时队列中不存在待执行的任务,NgZone会把自身状态修改为稳定态。

hasPendingMacrotaskshasPendingMicrotasks_nestingisStablelastRequestAnimationFrameId
truefalse0true-1
onHasTask

整个setTimeout跟踪的最后一步还是这个勾子,这次,勾子函数中会把hasPendingMacrotasks置为false。此时,几个状态已经恢复为最初的问题状态,Angular也在这个过程中执行了一次变更监测。

hasPendingMacrotaskshasPendingMicrotasks_nestingisStablelastRequestAnimationFrameId
falsefalse0true-1

MicroTask

MicroTask和MacroTask的行为大致上一致,只不过由于zone.js在处理MicroTask和MacroTask时有一丢丢的区别,导致这里处理也会有一点不同,这个我会在下文做专门解释。当然,如果你还想关注zone.js在处理MicroTask和MacroTask时到底有什么不一样的,可以关注一下我的下一篇文章(如果有的话),里面会像本系列的第三期一样,详细解释zone.js处理Promise的技术细节。

onHasTask

onHasTask跟之前没什么区别,执行过后,状态如下。与MacroTask不同,这次是hasPendingMicrotasks变为true,表示队列中有一个待执行的微任务。

传送门

onHasTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
  delegate.hasTask(target, hasTaskState);
  if (current === target) {
   if (hasTaskState.change == 'microTask') {
     zone._hasPendingMicrotasks = hasTaskState.microTask;
     // ...
   }
   // ...
  }
},
hasPendingMacrotaskshasPendingMicrotasks_nestingisStablelastRequestAnimationFrameId
falsetrue0true-1
onInvokeTask

MicroTask和MacroTask在这个勾子中的处理过程基本上是相同的。但是MicroTask在回调执行的时候和MacroTask还是有一点差异的。前面部分,我们讲MacroTask的时候,delegate.invokeTask(target, task, applyThis, applyArgs);这句会直接触发setTimeout的回调函数执行。但是在MicroTask中,微任务的回调外部还会包装一层zone.run,导致MicroTask的回调会通过onInvoke勾子执行。

传送门

onInvokeTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
 applyArgs: any): any => {
  try {
    onEnter(zone);
    // 执行真正的回调 👇
    return delegate.invokeTask(target, task, applyThis, applyArgs);
  } finally {
    // ...
    onLeave(zone);
  }
},
onInvoke

可以看到onInvokeonInvokeTask函数的内容是差不多的。onEnteronLeave的调用也基本一致,所以这里就不专门分析了。

onInvoke:
(delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
 applyArgs?: any[], source?: string): any => {
  try {
    onEnter(zone);
    return delegate.invoke(target, callback, applyThis, applyArgs, source);
  } finally {
    onLeave(zone);
  }
},

当这个函数执行结束后,几个状态值变化如下。

hasPendingMacrotaskshasPendingMicrotasks_nestingisStablelastRequestAnimationFrameId
falsetrue0true-1
onHasTask

最后一个执行的还是onHasTask函数,这个函数执行完毕后,几个状态又回到初始状态。

hasPendingMacrotaskshasPendingMicrotasks_nestingisStablelastRequestAnimationFrameId
falsefalse0true-1

Event

Event的执行方式跟MacroTask和MicroTask都不太一样。还记得之前我们在讲NgZone的5大状态的时候,有一个lastRequestAnimationFrameId一直没有用到。那么,在Event的处理过程中,我们会看到它的作用。

3_event_1679915878114.png

onInvokeTask

Event的处理入口是onInvokeTask而不是onHasTaskonEnterdelegate.invokeTask与之前都差不多,但是在finally子句中,你会发现Event的处理中多了一个delayChangeDetectionForEventsDelegate函数。其实从函数的函数名大概能猜个七七八八,这个是一个事件延时处理的函数。

onInvokeTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
 applyArgs: any): any => {
  try {
    onEnter(zone);
    return delegate.invokeTask(target, task, applyThis, applyArgs);
  } finally {
    if ((zone.shouldCoalesceEventChangeDetection && task.type === 'eventTask') ||
        zone.shouldCoalesceRunChangeDetection) {
      // Event的特殊处理逻辑 👇
      delayChangeDetectionForEventsDelegate();
    }
    onLeave(zone);
  }
},
delayChangeDetectionForEventsDelegate

其实我们在上一期讲解中已经介绍了一些通过NgZone进行性能调优的手段,那么这个函数的产生实际上也是用于性能上的优化。我们知道,浏览器很多事件诸如mousemove、scroll这些都会在短时间内产生大量的事件。如果每个这样的事件都会触发一次Angular的变更检测的话,那么对性能上的要求是很大的。所以,NgZone也需要在内部对于这些浏览器的事件做一些特殊处理,让大量的事件积攒一段时间后再统一做一次变更检测。

那么delayChangeDetectionForEventsDelegate中实际调用的方法是delayChangeDetectionForEvents,所以我们重点关注一下delayChangeDetectionForEvents函数的源码。

  1. 代码1这里,我们第一次见到对lastRequestAnimationFrameId的判断,当第一个Event到来时,这里的lastRequestAnimationFrameId还是初始值-1
hasPendingMacrotaskshasPendingMicrotasks_nestingisStablelastRequestAnimationFrameId
falsefalse1false-1
  1. zone.nativeRequestAnimationFrame的调用实际上调用的是Window.requestAnimationFrame。这里,NgZone实际上是希望通过requestAnimationFrame收集这一帧内的所有事件,在这一帧结束后,再统一执行一次变更检测。requestAnimationFrame执行的返回值会赋值给lastRequestAnimationFrameId,这样,在接下来代码每次进入到代码1处的时候,函数会直接返回。
  2. updateMicroTaskStatus被用来更新微任务状态的。那么这里执行之后,状态值中的hasPendingMicrotasks会变为true。这里这么做是为了收集Event的时候可以阻塞微任务触发变更检测,这么做的原因是为了确保Event事件的执行顺序不会被微任务打乱。这里要详细介绍又会有很大篇幅,感兴趣的可以自己看下这个issue;不想关注的可以先跳过这里。
hasPendingMacrotaskshasPendingMicrotasks_nestingisStablelastRequestAnimationFrameId
falsetrue1false一个正整数返回值
  1. 当当前帧执行完毕、下一帧要执行的时候会调用一次checkStable函数。这个函数在前面讲过,它是触发Angular变更检测的关键。通过执行该方法,Angular会通过ApplicationRef执行变更检测动作。

传送门

function delayChangeDetectionForEvents(zone: NgZonePrivate) {
  // 👇 1
  if (zone.isCheckStableRunning || zone.lastRequestAnimationFrameId !== -1) {
    return;
  }
  
  // 👇 2
  zone.lastRequestAnimationFrameId = zone.nativeRequestAnimationFrame.call(global, () => {
    if (!zone.fakeTopEventTask) {
      zone.fakeTopEventTask = Zone.root.scheduleEventTask('fakeTopEventTask', () => {
        zone.lastRequestAnimationFrameId = -1;
        updateMicroTaskStatus(zone);
        zone.isCheckStableRunning = true;
        // 👇 4
        checkStable(zone);
        zone.isCheckStableRunning = false;
      }, undefined, () => {}, () => {});
    }
    zone.fakeTopEventTask.invoke();
  });
  // 👇 3
  updateMicroTaskStatus(zone);
}

4_delay_1679915937924.jpg

再见ApplicationRef

上一节中,我们讲过一点ApplicationRef相关的知识,这一次,我们重点看下ApplicationRef跟变更检测相关的代码。

变更检测

前面说到,NgZone在checkStable中,如果发现当前已经没有待执行的任务的时候,会触发一个onMicrotaskEmpty事件。在这里,这个事件会被ApplicationRef所捕获。捕获后,会执行ApplicationRef.tick,而这个tick就是变更检测的入口。

传送门

this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({
  next: () => {
    this._zone.run(() => {
      this.tick();
    });
  },
});
tick

tick方法中,我们可以看到ApplicationRef通过调用视图的detectChanges方法,让组件完成自上而下的变更检测。上一篇文章中,我们介绍过一些手动执行变更检测的方法,其中有提到过ChangeDetectorRef.detectChanges()这个方法。这个方法可以对当前组件以及当前组件的子组件进行进行变更检测。那么这里看到的view.detectChanges()ChangeDetectorRef.detectChanges()又有什么关系?

其实从_views类型的继承链可以发现,_views的类型InternalViewRef继承自ViewRefViewRef又继承自ChangeDetectorRef。所以调用view.detectChanges()就相当于调用了ChangeDetectorRef.detectChanges(),从而完成一次自上而下的变更检测。

传送门

tick(): void {
  // ...
  try {
    this._runningTick = true;
    for (let view of this._views) {
      // 👇 组件的变更检测
      view.detectChanges();
    }
    if (typeof ngDevMode === 'undefined' || ngDevMode) {
      for (let view of this._views) {
        view.checkNoChanges();
      }
    }
  } catch (e) {
    // Attention: Don't rethrow as it could cancel subscriptions to Observables!
    this._zone.runOutsideAngular(() => this._exceptionHandler.handleError(e));
  } finally {
    this._runningTick = false;
  }
}

以上就是NgZone和ApplicationRef之间的配合关系。我们整体再回顾一下整个系列课程的内容。zone.js通过Monkey Patch对所有异步方法进行打包;打包后的异步方法被植入了很多勾子函数,而这些勾子函数可以被zone.js的上下文检测到,从而完成对异步任务的监控。

NgZone是对zone.js的一个使用案例,NgZone通过维护InnerZone和OuterZone两个Zone实现了对Angular应用中的异步任务的监控和去监控。NgZone同时在内部也维护了几个对异步任务监控的状态信息,通过这些信息实现了和ApplicationRef之间的“通信”,最终由ApplicationRef完成对Angular应用的监控。

本文小结

到这里,今天的内容就介绍的差不多了。最后,这里还需要像读者说明一点,在NgZone中跟踪Task的运行是一件比较难的事情,本文所有这些Task的举例其实都是理想化的。比如说,在举例setTimeout的时候,你会发现当你想在Angular应用中对异步Task跟踪的时候,会有很多其它Task同时在执行着,这些Task经常会在你调试跟踪的时候对你形成“干扰”。所以,本文的这些举例只是希望让大家看过后,能大致对每种不同任务在NgZone中流程有个认识,而真实的过程会远比我今天讲的内容复杂的多。这同时也从侧面反映出,zone.js默默对Angular作出多大的贡献。

大完结

5_change2_1679915789028.jpg

本系列分享历时将近1个多月,加上前期的一些分析和总结,我个人大概持续关注zone.js有两个多月了。最后的最后,我也分享几点个人感受:

  1. 有人说zone.js是暴力美学,我个人感觉可能美的地方更多一些吧。作为Angular变更检测的核心,Angular的变更检测在三大框架中是独一份的存在。我觉得比起其它两个通过数据劫持和虚拟Dom的方式进行数据绑定的方式,zone.js显得还是要温柔一些的。毕竟数据劫持是直接“污染”了数据的,而zone.js“改造”的是工具。我没法说谁更好,只是个人更偏向于后者。
  2. 截止到现在,我个人也没有完全看完zone.js的源码,但是我希望我会在后续的工作中持续关注这个产品。同时我也看到JiaLi(zone.js作者)为了他的作品不断地对zone.js进行改进。所以,请他加油,我希望zone.js可以越来越好!不过话说回来,JiaLi想在Angular社区完成一个PR是不是太难了点啊。我看了他好多的修改,经常要等好久才能审核通过,有点心疼他。🤣
  3. 其实最开始的时候,我只是想自己学学zone.js的,并没有规划这个系列分享。但是,我在学习源码的时候,苦于能找到的资料太旧又太少,所以就准备自己写一个有史以来最通俗、最全面、也最适合中国人学的zone.js材料。当然,前两个“最”我可能还不配;但是第三个最,我觉得还是可以搏一搏的✌。

OpenTiny Vue招募贡献者啦!

OpenTiny Vue 正在招募社区贡献者,欢迎加入我们🎉

你可以通过以下方式参与贡献:

  • 在 issue 列表中选择自己喜欢的任务
  • 阅读贡献者指南,开始参与贡献

你可以根据自己的喜好认领以下类型的任务:

  • 编写单元测试
  • 修复组件缺陷
  • 为组件添加新特性
  • 完善组件的文档

如何贡献单元测试:

  • packages/vue目录下搜索it.todo关键字,找到待补充的单元测试
  • 按照以上指南编写组件单元测试
  • 执行单个组件的单元测试:pnpm test:unit3 button

如果你是一位经验丰富的开发者,想接受一些有挑战的任务,可以考虑以下任务:

  • ✨ [Feature]: 希望提供 Skeleton 骨架屏组件
  • ✨ [Feature]: 希望提供 Divider 分割线组件
  • ✨ [Feature]: tree树形控件能增加虚拟滚动功能
  • ✨ [Feature]: 增加视频播放组件
  • ✨ [Feature]: 增加思维导图组件
  • ✨ [Feature]: 添加类似飞书的多维表格组件
  • ✨ [Feature]: 添加到 unplugin-vue-components
  • ✨ [Feature]: 兼容formily

参与 OpenTiny 开源社区贡献,你将收获:

直接的价值:

  1. 通过参与一个实际的跨端、跨框架组件库项目,学习最新的Vite+Vue3+TypeScript+Vitest技术
  2. 学习从 0 到 1 搭建一个自己的组件库的整套流程和方法论,包括组件库工程化、组件的设计和开发等
  3. 为自己的简历和职业生涯添彩,参与过优秀的开源项目,这本身就是受面试官青睐的亮点
  4. 结识一群优秀的、热爱学习、热爱开源的小伙伴,大家一起打造一个伟大的产品

长远的价值:

  1. 打造个人品牌,提升个人影响力
  2. 培养良好的编码习惯
  3. 获得华为云 OpenTiny 团队的荣誉和定制小礼物
  4. 受邀参加各类技术大会
  5. 成为 PMC 和 Committer 之后还能参与 OpenTiny 整个开源生态的决策和长远规划,培养自己的管理和规划能力
  6. 未来有更多机会和可能

其他说明

图片

OpenTiny 是一套企业级组件库解决方案,适配 PC 端 / 移动端等多端,涵盖 Vue2 / Vue3 / Angular 多技术栈,拥有主题配置系统 / 中后台模板 / CLI 命令行等效率提升工具,可帮助开发者高效开发 Web 应用。

核心亮点:

  • 跨端跨框架: 使用 Renderless 无渲染组件设计架构,实现了一套代码同时支持 Vue2 / Vue3,PC / Mobile 端,并支持函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强

  • 组件丰富:PC 端有100+组件,移动端有30+组件,包含高频组件 Table、Tree、Select 等,内置虚拟滚动,保证大数据场景下的流畅体验,除了业界常见组件之外,我们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP地址输入框、Calendar 日历、Crop 图片裁切等

  • 配置式组件: 组件支持模板式和配置式两种使用方式,适合低代码平台,目前团队已经将 OpenTiny 集成到内部的低代码平台,针对低码平台做了大量优化

  • 周边生态齐全: 提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供包含 10+ 实用功能、20+ 典型页面的 TinyPro 中后台模板,提供覆盖前端开发全流程的 TinyCLI 工程化工具,提供强大的在线主题配置平台 TinyTheme


欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网:opentiny.design/

OpenTiny 代码仓库https://github.com/opentiny/

Vue组件库opentiny.design/tiny-vue

Angular组件库opentiny.design/tiny-ng

欢迎进入代码仓库 Star🌟TinyVue、TinyNG、TinyCLI~

如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

往期文章推荐

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值