使用 TypeScript 和 Promises 的内容异步 Loading/Unloading
Async Loading/Unloading of content using TypeScript and Promises
我已经使用 TypeScript、Knockout、Generic Promises for TypeScript (https://github.com/pragmatrix/Promise) and async (https://github.com/caolan/async) 创建了异步 loading/unloading 内容框架。
虽然逻辑正常工作并且事件以正确的顺序触发和发生,但在加载 NavigationItem 时 UI 不会更新新选择并且不会在新项目上开始加载。谁能看出这是为什么?
核心逻辑在NavigationItemclass:
export class NavigationItem {
constructor(public Data: INavigationData) {
this.data = ko.observable(Data);
this.data.subscribe(n => Data = n);
this.status = ko.observable(NavigationItemStatus.Unloaded);
this.isLoading = ko.computed(() => this.status() == NavigationItemStatus.Loading);
this.isLoaded = ko.computed(() => this.status() == NavigationItemStatus.Loaded);
this.isUnloaded = ko.computed(() => this.status() == NavigationItemStatus.Unloaded);
this.isUnloading = ko.computed(() => this.status() == NavigationItemStatus.Unloading);
}
public data: KnockoutObservable<INavigationData>;
public status: KnockoutObservable<NavigationItemStatus>;
public isLoading: KnockoutComputed<boolean>;
public isLoaded: KnockoutComputed<boolean>;
public isUnloading: KnockoutComputed<boolean>;
public isUnloaded: KnockoutComputed<boolean>;
public closed: Lind.Events.ITypedEvent<NavigationItem> = new Lind.Events.TypedEvent();
public navigationItemAdded: Lind.Events.ITypedEvent<NavigationItem> = new Lind.Events.TypedEvent();
private queue: AsyncQueue<boolean> = async.queue((s, c) => {
if (s)
this.loadWorker().done(() => c());
else
this.unloadWorker().done(() => c());
}, 1);
load() : Promise<boolean>{
var d = defer<boolean>();
this.queue.push(true, () => d.resolve(true));
return d.promise();
}
unload() : Promise<boolean>{
var d = defer<boolean>();
this.queue.push(false, () => d.resolve(true));
return d.promise();
}
private unloadWorker(): Promise<boolean> {
var d = defer<boolean>();
this.doUnload().done(s => this.onUnloaded(s, d));
this.onUnloading();
return d.promise();
}
private loadWorker(): Promise<boolean>{
var d = defer<boolean>();
if (this.isLoaded())
{
this.unload();
this.load();
d.resolve(false);
}
else {
this.doLoad().done(s => this.onLoaded(s, d));
this.onLoading();
}
return d.promise();
}
private onLoaded(loadStatus: boolean, promise: P.Deferred<boolean>) {
this.status(NavigationItemStatus.Loaded);
promise.resolve(loadStatus);
}
private onUnloaded(unloadStatus: boolean, promise: P.Deferred<boolean>) {
this.status(NavigationItemStatus.Unloaded);
promise.resolve(unloadStatus);
}
private onLoading() {
this.status(NavigationItemStatus.Loading);
}
private onUnloading() {
this.status(NavigationItemStatus.Unloading);
}
doLoad(): Promise<boolean> {
var d = defer<boolean>();
d.resolve(true);
return d.promise();
}
doUnload(): Promise<boolean> {
var d = defer<boolean>();
d.resolve(true);
return d.promise();
}
close() {
if(this.status() != NavigationItemStatus.Unloaded)
this.unload();
this.closed.trigger(this);
}
addNavigationItem(navigationItem : NavigationItem) {
this.navigationItemAdded.trigger(navigationItem);
}
}
当调用 load() 时,它会将一个加载工作人员排入队列,当调用 unload() 时,它会将一个卸载工作人员排入队列,该队列的并发性为 1。NavigationItemCollection class 扩展了 NavigationItem,公开了一个可观察数组并实现 doLoad 和 doUnload。
export class NavigationItemCollection<T> extends NavigationItem {
constructor(data: INavigationData) {
super(data);
this.items = ko.observableArray<T>();
}
public items: KnockoutObservableArray<T>;
doLoad(): Promise<boolean> {
var d = defer<boolean>();
super.doLoad().done(() => {
this.getItems().done(i => {
if (i != null) {
for (var k: number = 0; k < i.length; k++) {
this.items.push(i[k]);
}
}
d.resolve(true);
});
});
return d.promise();
}
doUnload(): Promise<boolean> {
var d = defer<boolean>();
super.doUnload().done(() => {
this.items.removeAll();
d.resolve(true);
});
return d.promise();
}
getItems(): Promise<T[]> {
var d = defer<T[]>();
d.resolve(null);
return d.promise();
}
}
RepositoryNavigationItem class 然后实现 NavigationItemCollection 并实现 getItems()。
export class RepositoryNavigationItem<TViewModel, TEntity> extends ViewModels.Navigation.NavigationItemCollection<TViewModel>{
constructor(data: ViewModels.Navigation.INavigationData, public Repository: Northwind.Repository.IRepositoryGeneric<TEntity>) {
super(data);
}
getItems(): Promise<TViewModel[]> {
var d = defer<TViewModel[]>();
this.Repository.GetAll().done(i => {
var vms: TViewModel[] = [];
if (i != null) {
for (var k: number = 0; k < i.length; k++) {
vms.push(this.createViewModel(i[k]));
}
}
d.resolve(vms);
});
return d.promise();
}
createViewModel(entity : TEntity): TViewModel {
return null;
}
}
export class ProductsNavigationItem extends RepositoryNavigationItem<Northwind.Product, Northwind.IProduct>{
createViewModel(entity: Northwind.IProduct): Northwind.Product {
return Northwind.Product.Create(entity);
}
}
存储库实现如下:
export class Repository<TEntity> implements IRepositoryGeneric<TEntity>{
constructor(public ServiceLocation: string) { }
GetAll(): Promise<TEntity[]> {
var d = defer<TEntity[]>();
$.ajax({
type: "GET",
url: this.ServiceLocation + "GetAll",
success: data => d.resolve(<TEntity[]>data),
error: err => d.resolve(null)
});
return d.promise();
}
}
MainWindowViewModel 然后实例化 NavigationItems(使用 IoC 依赖项注入)并在选择和取消选择 NavigationItems 时控制 load/unload 控制流。
export class MainWindowViewModel {
constructor(private Container: Lind.IoC.IContainer, navigationData: ViewModels.Navigation.INavigationData[]) {
this.navigationItems = ko.observableArray<ViewModels.Navigation.NavigationItem>();
this.selectedNavigationItem = ko.observable<ViewModels.Navigation.NavigationItem>();
this.selectedNavigationItemType = ko.computed(() => {
var navItem = this.selectedNavigationItem();
if (navItem != null)
return navItem.data().Name;
return "Loading";
});
this.selectedNavigationItem.subscribe(n => {
if (n != null)
n.unload();
}, this, "beforeChange");
this.selectedNavigationItem.subscribe(n => {
if(n != null)
n.load();
});
for (var i: number = 0; i < navigationData.length; i++) {
var navItem = Container.Resolve<ViewModels.Navigation.NavigationItem>(typeof ViewModels.Navigation.NavigationItem, navigationData[i].Name,
[new Lind.IoC.ConstructorParameterFactory("data", () => navigationData[i])]);
navItem.closed.add(this.onNavigationItemClosed);
navItem.navigationItemAdded.add(this.onNavigationItemAdded);
this.navigationItems.push(navItem);
}
this.selectedNavigationItem(this.navigationItems.peek()[0]);
}
public navigationItems: KnockoutObservableArray<ViewModels.Navigation.NavigationItem>;
public selectedNavigationItem: KnockoutObservable<ViewModels.Navigation.NavigationItem>;
public selectedNavigationItemType: KnockoutComputed<string>;
private onNavigationItemClosed(item: ViewModels.Navigation.NavigationItem) {
item.closed.remove(this.onNavigationItemClosed);
item.navigationItemAdded.remove(this.onNavigationItemAdded);
this.navigationItems.remove(item);
if (this.selectedNavigationItem() == item)
this.selectedNavigationItem(this.navigationItems.peek()[0]);
}
private onNavigationItemAdded(item: ViewModels.Navigation.NavigationItem) {
item.navigationItemAdded.add(this.onNavigationItemAdded);
item.closed.add(this.onNavigationItemClosed);
this.navigationItems.push(item);
this.selectedNavigationItem(item);
}
}
然后视图如下:
<div id="rightNav" style="float:left">
<ul data-bind="foreach: navigationItems">
<li>
<div data-bind="style:{ background: isLoading() == true ? 'yellow' : (isLoaded() == true ? 'green' : (isUnloading() == true ? 'gray' : (isUnloaded() == true ? 'white' : 'red')))}">
<span><a data-bind="text:data().DisplayName, click: $parent.selectedNavigationItem"></a><button data-bind="visible:data().IsCloseable == true, click: close" >x</button></span>
</div>
</li>
</ul>
</div>
<div id="leftContent" data-bind="template: { name: selectedNavigationItemType(), data: selectedNavigationItem }"></div>
<script type="text/html" id="Products">
<table>
<thead>
<tr>
<th>Name</th>
<th>Supplier</th>
<th>Category</th>
<th>Unit Price</th>
<th>Units in Stock</th>
<th>Discontinued</th>
</tr>
</thead>
<tbody data-bind="foreach: items">
<tr>
<td><span data-bind="text:productName" /></td>
<td><span data-bind="text: supplier().companyName" /></td>
<td><span data-bind="text: category().categoryName" /></td>
<td><span data-bind="text: unitPrice" /></td>
<td><span data-bind="text: unitsInStock" /></td>
<td><input type="checkbox" data-bind="checked: discontinued" disabled="disabled" /></td>
</tr>
</tbody>
</table>
</script>
我添加了一个模拟存储库并使用模拟视图对其进行了测试:
module Northwind.Repository.Mock {
export class MockRepository<TEntity> implements IRepositoryGeneric<TEntity>{
constructor(public ServiceLocation: string) { }
Delete(id: number): Promise<boolean> {
var d = defer<boolean>();
d.resolve(null);
return d.promise();
}
GetAll(): Promise<TEntity[]> {
var d = defer<TEntity[]>();
setTimeout(() => {
d.resolve(null);
}, 5000);
return d.promise();
}
Get(id: number): Promise<TEntity> {
var d = defer<TEntity>();
d.resolve(null);
return d.promise();
}
Add(entity: TEntity): Promise<TEntity> {
var d = defer<TEntity>();
d.resolve(null);
return d.promise();
}
Update(entity: TEntity): Promise<boolean> {
var d = defer<boolean>();
d.resolve(false);
return d.promise();
}
}
}
当正确模拟此功能时,看起来 jQuery ajax 调用以某种方式阻塞,这是怎么回事?
这是一个 IE 错误!什么鬼?
我已经使用 TypeScript、Knockout、Generic Promises for TypeScript (https://github.com/pragmatrix/Promise) and async (https://github.com/caolan/async) 创建了异步 loading/unloading 内容框架。
虽然逻辑正常工作并且事件以正确的顺序触发和发生,但在加载 NavigationItem 时 UI 不会更新新选择并且不会在新项目上开始加载。谁能看出这是为什么?
核心逻辑在NavigationItemclass:
export class NavigationItem {
constructor(public Data: INavigationData) {
this.data = ko.observable(Data);
this.data.subscribe(n => Data = n);
this.status = ko.observable(NavigationItemStatus.Unloaded);
this.isLoading = ko.computed(() => this.status() == NavigationItemStatus.Loading);
this.isLoaded = ko.computed(() => this.status() == NavigationItemStatus.Loaded);
this.isUnloaded = ko.computed(() => this.status() == NavigationItemStatus.Unloaded);
this.isUnloading = ko.computed(() => this.status() == NavigationItemStatus.Unloading);
}
public data: KnockoutObservable<INavigationData>;
public status: KnockoutObservable<NavigationItemStatus>;
public isLoading: KnockoutComputed<boolean>;
public isLoaded: KnockoutComputed<boolean>;
public isUnloading: KnockoutComputed<boolean>;
public isUnloaded: KnockoutComputed<boolean>;
public closed: Lind.Events.ITypedEvent<NavigationItem> = new Lind.Events.TypedEvent();
public navigationItemAdded: Lind.Events.ITypedEvent<NavigationItem> = new Lind.Events.TypedEvent();
private queue: AsyncQueue<boolean> = async.queue((s, c) => {
if (s)
this.loadWorker().done(() => c());
else
this.unloadWorker().done(() => c());
}, 1);
load() : Promise<boolean>{
var d = defer<boolean>();
this.queue.push(true, () => d.resolve(true));
return d.promise();
}
unload() : Promise<boolean>{
var d = defer<boolean>();
this.queue.push(false, () => d.resolve(true));
return d.promise();
}
private unloadWorker(): Promise<boolean> {
var d = defer<boolean>();
this.doUnload().done(s => this.onUnloaded(s, d));
this.onUnloading();
return d.promise();
}
private loadWorker(): Promise<boolean>{
var d = defer<boolean>();
if (this.isLoaded())
{
this.unload();
this.load();
d.resolve(false);
}
else {
this.doLoad().done(s => this.onLoaded(s, d));
this.onLoading();
}
return d.promise();
}
private onLoaded(loadStatus: boolean, promise: P.Deferred<boolean>) {
this.status(NavigationItemStatus.Loaded);
promise.resolve(loadStatus);
}
private onUnloaded(unloadStatus: boolean, promise: P.Deferred<boolean>) {
this.status(NavigationItemStatus.Unloaded);
promise.resolve(unloadStatus);
}
private onLoading() {
this.status(NavigationItemStatus.Loading);
}
private onUnloading() {
this.status(NavigationItemStatus.Unloading);
}
doLoad(): Promise<boolean> {
var d = defer<boolean>();
d.resolve(true);
return d.promise();
}
doUnload(): Promise<boolean> {
var d = defer<boolean>();
d.resolve(true);
return d.promise();
}
close() {
if(this.status() != NavigationItemStatus.Unloaded)
this.unload();
this.closed.trigger(this);
}
addNavigationItem(navigationItem : NavigationItem) {
this.navigationItemAdded.trigger(navigationItem);
}
}
当调用 load() 时,它会将一个加载工作人员排入队列,当调用 unload() 时,它会将一个卸载工作人员排入队列,该队列的并发性为 1。NavigationItemCollection class 扩展了 NavigationItem,公开了一个可观察数组并实现 doLoad 和 doUnload。
export class NavigationItemCollection<T> extends NavigationItem {
constructor(data: INavigationData) {
super(data);
this.items = ko.observableArray<T>();
}
public items: KnockoutObservableArray<T>;
doLoad(): Promise<boolean> {
var d = defer<boolean>();
super.doLoad().done(() => {
this.getItems().done(i => {
if (i != null) {
for (var k: number = 0; k < i.length; k++) {
this.items.push(i[k]);
}
}
d.resolve(true);
});
});
return d.promise();
}
doUnload(): Promise<boolean> {
var d = defer<boolean>();
super.doUnload().done(() => {
this.items.removeAll();
d.resolve(true);
});
return d.promise();
}
getItems(): Promise<T[]> {
var d = defer<T[]>();
d.resolve(null);
return d.promise();
}
}
RepositoryNavigationItem class 然后实现 NavigationItemCollection 并实现 getItems()。
export class RepositoryNavigationItem<TViewModel, TEntity> extends ViewModels.Navigation.NavigationItemCollection<TViewModel>{
constructor(data: ViewModels.Navigation.INavigationData, public Repository: Northwind.Repository.IRepositoryGeneric<TEntity>) {
super(data);
}
getItems(): Promise<TViewModel[]> {
var d = defer<TViewModel[]>();
this.Repository.GetAll().done(i => {
var vms: TViewModel[] = [];
if (i != null) {
for (var k: number = 0; k < i.length; k++) {
vms.push(this.createViewModel(i[k]));
}
}
d.resolve(vms);
});
return d.promise();
}
createViewModel(entity : TEntity): TViewModel {
return null;
}
}
export class ProductsNavigationItem extends RepositoryNavigationItem<Northwind.Product, Northwind.IProduct>{
createViewModel(entity: Northwind.IProduct): Northwind.Product {
return Northwind.Product.Create(entity);
}
}
存储库实现如下:
export class Repository<TEntity> implements IRepositoryGeneric<TEntity>{
constructor(public ServiceLocation: string) { }
GetAll(): Promise<TEntity[]> {
var d = defer<TEntity[]>();
$.ajax({
type: "GET",
url: this.ServiceLocation + "GetAll",
success: data => d.resolve(<TEntity[]>data),
error: err => d.resolve(null)
});
return d.promise();
}
}
MainWindowViewModel 然后实例化 NavigationItems(使用 IoC 依赖项注入)并在选择和取消选择 NavigationItems 时控制 load/unload 控制流。
export class MainWindowViewModel {
constructor(private Container: Lind.IoC.IContainer, navigationData: ViewModels.Navigation.INavigationData[]) {
this.navigationItems = ko.observableArray<ViewModels.Navigation.NavigationItem>();
this.selectedNavigationItem = ko.observable<ViewModels.Navigation.NavigationItem>();
this.selectedNavigationItemType = ko.computed(() => {
var navItem = this.selectedNavigationItem();
if (navItem != null)
return navItem.data().Name;
return "Loading";
});
this.selectedNavigationItem.subscribe(n => {
if (n != null)
n.unload();
}, this, "beforeChange");
this.selectedNavigationItem.subscribe(n => {
if(n != null)
n.load();
});
for (var i: number = 0; i < navigationData.length; i++) {
var navItem = Container.Resolve<ViewModels.Navigation.NavigationItem>(typeof ViewModels.Navigation.NavigationItem, navigationData[i].Name,
[new Lind.IoC.ConstructorParameterFactory("data", () => navigationData[i])]);
navItem.closed.add(this.onNavigationItemClosed);
navItem.navigationItemAdded.add(this.onNavigationItemAdded);
this.navigationItems.push(navItem);
}
this.selectedNavigationItem(this.navigationItems.peek()[0]);
}
public navigationItems: KnockoutObservableArray<ViewModels.Navigation.NavigationItem>;
public selectedNavigationItem: KnockoutObservable<ViewModels.Navigation.NavigationItem>;
public selectedNavigationItemType: KnockoutComputed<string>;
private onNavigationItemClosed(item: ViewModels.Navigation.NavigationItem) {
item.closed.remove(this.onNavigationItemClosed);
item.navigationItemAdded.remove(this.onNavigationItemAdded);
this.navigationItems.remove(item);
if (this.selectedNavigationItem() == item)
this.selectedNavigationItem(this.navigationItems.peek()[0]);
}
private onNavigationItemAdded(item: ViewModels.Navigation.NavigationItem) {
item.navigationItemAdded.add(this.onNavigationItemAdded);
item.closed.add(this.onNavigationItemClosed);
this.navigationItems.push(item);
this.selectedNavigationItem(item);
}
}
然后视图如下:
<div id="rightNav" style="float:left">
<ul data-bind="foreach: navigationItems">
<li>
<div data-bind="style:{ background: isLoading() == true ? 'yellow' : (isLoaded() == true ? 'green' : (isUnloading() == true ? 'gray' : (isUnloaded() == true ? 'white' : 'red')))}">
<span><a data-bind="text:data().DisplayName, click: $parent.selectedNavigationItem"></a><button data-bind="visible:data().IsCloseable == true, click: close" >x</button></span>
</div>
</li>
</ul>
</div>
<div id="leftContent" data-bind="template: { name: selectedNavigationItemType(), data: selectedNavigationItem }"></div>
<script type="text/html" id="Products">
<table>
<thead>
<tr>
<th>Name</th>
<th>Supplier</th>
<th>Category</th>
<th>Unit Price</th>
<th>Units in Stock</th>
<th>Discontinued</th>
</tr>
</thead>
<tbody data-bind="foreach: items">
<tr>
<td><span data-bind="text:productName" /></td>
<td><span data-bind="text: supplier().companyName" /></td>
<td><span data-bind="text: category().categoryName" /></td>
<td><span data-bind="text: unitPrice" /></td>
<td><span data-bind="text: unitsInStock" /></td>
<td><input type="checkbox" data-bind="checked: discontinued" disabled="disabled" /></td>
</tr>
</tbody>
</table>
</script>
我添加了一个模拟存储库并使用模拟视图对其进行了测试:
module Northwind.Repository.Mock {
export class MockRepository<TEntity> implements IRepositoryGeneric<TEntity>{
constructor(public ServiceLocation: string) { }
Delete(id: number): Promise<boolean> {
var d = defer<boolean>();
d.resolve(null);
return d.promise();
}
GetAll(): Promise<TEntity[]> {
var d = defer<TEntity[]>();
setTimeout(() => {
d.resolve(null);
}, 5000);
return d.promise();
}
Get(id: number): Promise<TEntity> {
var d = defer<TEntity>();
d.resolve(null);
return d.promise();
}
Add(entity: TEntity): Promise<TEntity> {
var d = defer<TEntity>();
d.resolve(null);
return d.promise();
}
Update(entity: TEntity): Promise<boolean> {
var d = defer<boolean>();
d.resolve(false);
return d.promise();
}
}
}
当正确模拟此功能时,看起来 jQuery ajax 调用以某种方式阻塞,这是怎么回事?
这是一个 IE 错误!什么鬼?