[Abp vNext 源码分析] - 11. 用户的自定义参数与配置
一、简要说明
文章信息:
基于的 ABP vNext 版本:1.0.0
创作日期:2019 年 10 月 23 日晚
更新日期:2019 年 10 月 24 日
ABP vNext 针对用户可编辑的配置,提供了单独的 Volo.Abp.Settings 模块,本篇文章的后面都将这种用户可变更的配置,叫做 参数。所谓可编辑的配置,就是我们在系统页面上,用户可以动态更改的参数值。
例如你做的系统是一个门户网站,那么前端页面上展示的 Title ,你可以在后台进行配置。这个时候你就可以将网站这种全局配置作为一个参数,在程序代码中进行定义。通过 GlobalSettingValueProvider(后面会讲) 作为这个参数的值提供者,用户就可以随时对 Title 进行更改。又或者是某些通知的开关,你也可以定义一堆参数,让用户可以动态的进行变更。
二、源码分析
模块启动流程
AbpSettingsModule 模块干的事情只有两件,第一是扫描所有 ISettingDefinitionProvider (参数定义提供者),第二则是往配置参数添加一堆参数值提供者(ISettingValueProvider)。
public class AbpSettingsModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
// 自动扫描所有实现了 ISettingDefinitionProvider 的类型。
AutoAddDefinitionProviders(context.Services);
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
// 配置默认的一堆参数值提供者。
Configure<AbpSettingOptions>(options =>
{
options.ValueProviders.Add<DefaultValueSettingValueProvider>();
options.ValueProviders.Add<GlobalSettingValueProvider>();
options.ValueProviders.Add<TenantSettingValueProvider>();
options.ValueProviders.Add<UserSettingValueProvider>();
});
}
private static void AutoAddDefinitionProviders(IServiceCollection services)
{
var definitionProviders = new List<Type>();
services.OnRegistred(context =>
{
if (typeof(ISettingDefinitionProvider).IsAssignableFrom(context.ImplementationType))
{
definitionProviders.Add(context.ImplementationType);
}
});
// 将扫描到的数据添加到 Options 中。
services.Configure<AbpSettingOptions>(options =>
{
options.DefinitionProviders.AddIfNotContains(definitionProviders);
});
}
}
参数的定义
参数的基本定义
ABP vNext 关于参数的定义在类型 SettingDefinition 可以找到,内部的结构与 PermissionDefine 类似。。开发人员需要先定义有哪些可配置的参数,然后 ABP vNext 会自动进行管理,在网站运行期间,用户、租户可以根据自己的需要随时变更参数值。
public class SettingDefinition
{
/// <summary>
/// 参数的唯一标识。
/// </summary>
[NotNull]
public string Name { get; }
// 参数的显示名称,是一个多语言字符串。
[NotNull]
public ILocalizableString DisplayName
{
get => _displayName;
set => _displayName = Check.NotNull(value, nameof(value));
}
private ILocalizableString _displayName;
// 参数的描述信息,也是一个多语言字符串。
[CanBeNull]
public ILocalizableString Description { get; set; }
/// <summary>
/// 参数的默认值。
/// </summary>
[CanBeNull]
public string DefaultValue { get; set; }
/// <summary>
/// 指定参数与其参数的值,是否能够在客户端进行显示。对于某些密钥设置来说是很危险的,默认值为 Fasle。
/// </summary>
public bool IsVisibleToClients { get; set; }
/// <summary>
/// 允许更改本参数的值提供者,为空则允许所有提供者提供参数值。
/// </summary>
public List<string> Providers { get; } //TODO: 考虑重命名为 AllowedProviders。
/// <summary>
/// 当前参数是否能够继承父类的 Scope 信息,默认值为 True。
/// </summary>
public bool IsInherited { get; set; }
/// <summary>
/// 参数相关连的一些扩展属性,通过一个字典进行存储。
/// </summary>
[NotNull]
public Dictionary<string, object> Properties { get; }
/// <summary>
/// 参数的值是否以加密的形式存储,默认值为 False。
/// </summary>
public bool IsEncrypted { get; set; }
public SettingDefinition(
string name,
string defaultValue = null,
ILocalizableString displayName = null,
ILocalizableString description = null,
bool isVisibleToClients = false,
bool isInherited = true,
bool isEncrypted = false)
{
Name = name;
DefaultValue = defaultValue;
IsVisibleToClients = isVisibleToClients;
DisplayName = displayName ?? new FixedLocalizableString(name);
Description = description;
IsInherited = isInherited;
IsEncrypted = isEncrypted;
Properties = new Dictionary<string, object>();
Providers = new List<string>();
}
// 设置附加数据值。
public virtual SettingDefinition WithProperty(string key, object value)
{
Properties[key] = value;
return this;
}
// 设置 Provider 属性的值。
public virtual SettingDefinition WithProviders(params string[] providers)
{
if (!providers.IsNullOrEmpty())
{
Providers.AddRange(providers);
}
return this;
}
}
上面的参数定义值得注意的就是 DefaultValue 、IsVisibleToClients、IsEncrypted 这三个属性。默认值一般适用于某些系统配置,例如当前系统的默认语言。后面两个属性则更加注重于 安全问题,因为某些参数存储的是一些重要信息,这个时候就需要进行特殊处理了。
如果参数值是加密的,那么在获取参数值的时候就会进行解密操作,例如下面的代码。
SettingProvider 类中的相关代码:
// ...
public class SettingProvider : ISettingProvider, ITransientDependency
{
// ...
public virtual async Task<string> GetOrNullAsync(string name)
{
// ...
var value = await GetOrNullValueFromProvidersAsync(providers, setting);
// 对值进行解密处理。
if (setting.IsEncrypted)
{
value = SettingEncryptionService.Decrypt(setting, value);
}
return value;
}
// ...
}
参数不对客户端可见的话,在默认的 AbpApplicationConfigurationAppService 服务类中,获取参数值的时候就会跳过。
private async Task<ApplicationSettingConfigurationDto> GetSettingConfigAsync()
{
var result = new ApplicationSettingConfigurationDto
{
Values = new Dictionary<string, string>()
};
foreach (var settingDefinition in _settingDefinitionManager.GetAll())
{
// 不会展示这些属性为 False 的参数。
if (!settingDefinition.IsVisibleToClients)
{
continue;
}
result.Values[settingDefinition.Name] = await _settingProvider.GetOrNullAsync(settingDefinition.Name);
}
return result;
}
参数定义的扫描
跟权限定义类似,所有的参数定义都被放在了 SettingDefinitionProvider 里面,如果你需要定义一堆参数,只需要继承并实现 Define(ISettingDefinitionContext) 抽象方法就可以了。
public class TestSettingDefinitionProvider : SettingDefinitionProvider
{
public override void Define(ISettingDefinitionContext context)
{
context.Add(
new SettingDefinition(TestSettingNames.TestSettingWithoutDefaultValue),
new SettingDefinition(TestSettingNames.TestSettingWithDefaultValue, "default-value"),
new SettingDefinition(TestSettingNames.TestSettingEncrypted, isEncrypted: true)
);
}
}
因为我们的 SettingDefinitionProvider 实现了 ISettingDefinitionProvider 和 ITransientDependency 接口,所以这些 Provider 都会在组件注册的时候(模块里面有定义),添加到对应的 AbpSettingOptions 内部,方便后续进行调用。
参数定义的管理
我们的 参数定义提供者 和 参数值提供者 都赋值给 AbpSettingOptions 了,首先看有哪些地方使用到了 参数定义提供者。

第二个我们已经看过,是在模块启动时有用到。第一个则是有一个 SettingDefinitionManager ,顾名思义就是管理所有的 SettingDefinition 的管理器。这个管理器提供了三个方法,都是针对 SettingDefinition 的查询功能。
public interface ISettingDefinitionManager
{
// 根据参数定义的标识查询,不存在则抛出 AbpException 异常。
[NotNull]
SettingDefinition Get([NotNull] string name);
// 获得所有的参数定义。
IReadOnlyList<SettingDefinition> GetAll();
// 根据参数定义的标识查询,如果不存在则返回 null。
SettingDefinition GetOrNull(string name);
}
接下来我们看一下它的默认实现 SettingDefinitionManager ,它的内部没什么说的,只是注意 SettingDefinitions 的填充方式,这里使用了线程安全的 懒加载模式。只有当用到的时候,才会调用 CreateSettingDefinitions() 方法填充数据。
public class SettingDefinitionManager : ISettingDefinitionManager, ISingletonDependency
{
protected Lazy<IDictionary<string, SettingDefinition>> SettingDefinitions { get; }
protected AbpSettingOptions Options { get; }
protected IServiceProvider ServiceProvider { get; }
public SettingDefinitionManager(
IOptions<AbpSettingOptions> options,
IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Options = options.Value;
// 填充的时候,调用 CreateSettingDefinitions 方法进行填充。
SettingDefinitions = new Lazy<IDictionary<string, SettingDefinition>>(CreateSettingDefinitions, true);
}
// ...
protected virtual IDictionary<string, SettingDefinition> CreateSettingDefinitions()
{
var settings = new Dictionary<string, SettingDefinition>();
using (var scope = ServiceProvider.CreateScope())
{
// 从 Options 中得到类型,然后通过 IoC 进行实例化。
var providers = Options
.DefinitionProviders
.Select(p => scope.ServiceProvider.GetRequiredService(p) as ISettingDefinitionProvider)
.ToList();
// 执行每个 Provider 的 Define 方法填充数据。
foreach (var provider in providers)
{
provider.Define(new SettingDefinitionContext(settings));
}
}
return settings;
}
}
参数值的管理
当我们构建好参数的定义之后,我们要设置某个参数的值,或者说获取某个参数的值应该怎么操作呢?查看相关的单元测试,看到了 ABP vNext 自身是注入 ISettingProvider ,调用它的 GetOrNullAsync() 获取参数值。
private readonly ISettingProvider _settingProvider;
var settingValue = await _settingProvider.GetOrNullAsync("WebSite.Title")
跳转到接口,发现它有两个实现,这里我们只讲解一下 SettingProvider 类的实现。

获取参数值
直奔主题,来看一下 ISettingProvider.GetOrNullAsync(string) 方法是怎么来获取参数值的。
public class SettingProvider : ISettingProvider, ITransientDependency
{
protected ISettingDefinitionManager SettingDefinitionManager { get; }
protected ISettingEncryptionService SettingEncryptionService { get; }
protected ISettingValueProviderManager SettingValueProviderManager { get; }
public SettingProvider(
ISettingDefinitionManager settingDefinitionManager,
ISettingEncryptionService settingEncryptionService,
ISettingValueProviderManager settingValueProviderManager)
{
SettingDefinitionManager = settingDefinitionManager;
SettingEncryptionService = settingEncryptionService;
SettingValueProviderManager = settingValueProviderManager;
}
public virtual async Task<string> GetOrNullAsync(string name)
{
// 根据名称获取参数定义。
var setting = SettingDefinitionManager.Get(name);
// 从参数值提供者管理器,获得一堆参数值提供者。
var providers = Enumerable
.Reverse(SettingValueProviderManager.Providers);
// 过滤符合参数定义的提供者,这里就是用到了之前参数定义的 List<string> Providers 属性。
if (setting.Providers.Any())
{
providers = providers.Where(p => setting.Providers.Contains(p.Name));
}
//TODO: How to implement setting.IsInherited?
//TODO: 如何实现 setting.IsInherited 功能?
var value = await GetOrNullValueFromProvidersAsync(providers, setting);
// 如果参数是加密的,则需要进行解密操作。
if (setting.IsEncrypted)
{
value = SettingEncryptionService.Decrypt(setting, value);
}
return value;
}
protected virtual async Task<string> GetOrNullValueFromProvidersAsync(IEnumerable<ISettingValueProvider> providers,
SettingDefinition setting)
{
// 只要从任意 Provider 中,读取到了参数值,就直接进行返回。
foreach (var provider in providers)
{
var value = await provider.GetOrNullAsync(setting);
if (value != null)
{
return value;
}
}
return null;
}
// ...
}
所以真正干活的还是 ISettingValueProviderManager 里面存放的一堆 ISettingValueProvider ,这个 参数值管理器 的接口很简单,只提供了一个 List<ISettingValueProvider> Providers { get; } 的定义。
它会从模块配置的 ValueProviders 属性内部,通过 IoC 实例化对应的参数值提供者。
_lazyProviders = new Lazy<List<ISettingValueProvider>>(
() => Options
.ValueProviders
.Select(type => serviceProvider.GetRequiredService(type) as ISettingValueProvider)
.ToList(),
true
参数值提供者
参数值提供者的接口定义是 ISettingValueProvider,它定义了一个名称和 GetOrNullAsync(SettingDefinition) 方法,后者可以通过参数定义获取存储的值。
public interface ISettingValueProvider
{
string Name { get; }
Task<string> GetOrNullAsync([NotNull] SettingDefinition setting);
}
注意这里的返回值是 Task<string> ,也就是说我们的参数值类型必须是 string 类型的,如果需要存储其他的类型可能就需要从 string 进行类型转换了。
在这里的 SettingValueProvider 其实类似于我们之前讲过的 权限提供者。因为 ABP vNext 考虑到了多种情况,我们的参数值有可能是根据用户获取的,同时也有可能是根据不同的租户进行获取的。所以 ABP vNext 为我们预先定义了四种参数值提供器,他们分别是 DefaultValueSettingValueProvider、GlobalSettingValueProvider、TenantSettingValueProvider、UserSettingValueProvider 。

下面我们就来讲讲这几个不同的参数提供者有啥不一样。
DefaultValueSettingValueProvider:
顾名思义,默认值参数提供者就是使用的参数定义里面的 DefaultValue 属性,当你查询某个参数值的时候,就直接返回了。
public override Task<string> GetOrNullAsync(SettingDefinition setting)
{
return Task.FromResult(setting.DefaultValue);
}
GlobalSettingValueProvider:
这是一种全局的提供者,它没有对应的 Key,也就是说如果数据库能查到 ProviderName 是 G 的记录,就直接返回它的值了。
public class GlobalSettingValueProvider : SettingValueProvider
{
public const string ProviderName = "G";
public override string Name => ProviderName;
public GlobalSettingValueProvider(ISettingStore settingStore)
: base(settingStore)
{
}
public override Task<string> GetOrNullAsync(SettingDefinition setting)
{
return SettingStore.GetOrNullAsync(setting.Name, Name, null);
}
}
TenantSettingValueProvider:
租户提供者,则是会将当前登录租户的 Id 结合 T 进行查询,也就是参数值是按照不同的租户进行隔离的。
public class TenantSettingValueProvider : SettingValueProvider
{
public const string ProviderName = "T";
public override string Name => ProviderName;
protected ICurrentTenant CurrentTenant { get; }
public TenantSettingValueProvider(ISettingStore settingStore, ICurrentTenant currentTenant)
: base(settingStore)
{
CurrentTenant = currentTenant;
}
public override async Task<string> GetOrNullAsync(SettingDefinition setting)
{
return await SettingStore.GetOrNullAsync(setting.Name, Name, CurrentTenant.Id?.ToString());
}
}
UserSettingValueProvider:
用户提供者,则是会将当前用户的 Id 作为查询条件,结合 U 在数据库进行查询匹配的参数值,参数值是根据不同的用户进行隔离的。
public class UserSettingValueProvider : SettingValueProvider
{
public const string ProviderName = "U";
public override string Name => ProviderName;
protected ICurrentUser CurrentUser { get; }
public UserSettingValueProvider(ISettingStore settingStore, ICurrentUser currentUser)
: base(settingStore)
{
CurrentUser = currentUser;
}
public override async Task<string> GetOrNullAsync(SettingDefinition setting)
{
if (CurrentUser.Id == null)
{
return null;
}
return await SettingStore.GetOrNullAsync(setting.Name, Name, CurrentUser.Id.ToString());
}
}
参数值的存储
除了 DefaultValueSettingValueProvider 是直接从参数定义获取值以外,其他的参数值提供者都是通过 ISettingStore 读取参数值的。在该模块的默认实现当中,是直接返回 null 的,只有当你使用了 Volo.Abp.SettingManagement 模块,你的参数值才是存储到数据库当中的。
我这里不再详细解析 Volo.Abp.SettingManagement 模块的其他实现,只说一下 ISettingStore 在它内部的实现 SettingStore。
public class SettingStore : ISettingStore, ITransientDependency
{
protected ISettingManagementStore ManagementStore { get; }
public SettingStore(ISettingManagementStore managementStore)
{
ManagementStore = managementStore;
}
public Task<string> GetOrNullAsync(string name, string providerName, string providerKey)
{
return ManagementStore.GetOrNullAsync(name, providerName, providerKey);
}
}
我们可以看到它也只是个包装,真正的操作类型是 ISettingManagementStore。
参数值的设置
在 ABP vNext 的核心模块当中,是没有提供对参数值的变更的。只有在 Volo.Abp.SettingManagement 模块内部,它提供了 ISettingManager 管理器,可以进行参数值的变更。原理很简单,就是对数据库对应的表进行修改而已。
public async Task SetAsync(string name, string value, string providerName, string providerKey)
{
// 操作仓储,查询记录。
var setting = await SettingRepository.FindAsync(name, providerName, providerKey);
// 新增或者更新记录。
if (setting == null)
{
setting = new Setting(GuidGenerator.Create(), name, value, providerName, providerKey);
await SettingRepository.InsertAsync(setting);
}
else
{
setting.Value = value;
await SettingRepository.UpdateAsync(setting);
}
}
三、总结
ABP vNext 提供了多种参数值提供者,我们可以根据自己的需要灵活选择。如果不能够满足你的需求,你也可以自己实现一个参数值提供者。我建议对于用户在界面可更改的参数,都可以使用 SettingDefinition 定义成参数,可以根据不同的情况进行配置读取。
ABP vNext 其他模块用到的许多参数,也都是使用的 SettingDefinition 进行定义。例如 Identity 模块用到的密码验证规则,就是通过 ISettingProvider 进行读取的,还有当前程序的默认语言。
需要看其他的 ABP vNext 相关文章?点击我 即可跳转到总目录。
下面附上 E2Home 的总结,很详细:
在各个模块中定义设置数据源的类来设定配置键值对, 该类只需要继承接口
ISettingDefinitionProvider或者SettingDefinitionProvider实现类
ABP 会自动寻找被注册,最后会将配置键值对都汇总到SettingProvider类中。如果是存储在数据库中的,则需要重写ISettingStore
当然建议依赖 Volo.Abp.SettingManagement.Domain 这个模块,如果数据表是用自定义的,则建议重写ISettingRepository接口即可。在
ConfigureServices()方法中注册添加ISettingValueProvider,比如:值是 json 格式的,就可以定义一个设置值 Provider 来解析。ISettingValueProvider可以有多个,并且按倒序进行执行,只要能获取到值就返回,不再继续往下执行。一般自定义的 ISettingValueProvider 放在后面。如果将敏感数据保存到设置管理,则建议采用加密的方式,只需要重写
ISettingEncryptionService即可。 参数定义:IsEncrypted = true。Volo.Abp.SettingManagement.Domain 是采用数据库加缓存的方式来读写设置的,
通过SettingCacheItemInvalidator来注册 Setting 实体的EntityChanged事件,从而达到缓存能跟实体同步更新。为啥 ABP 还需要设置管理,而不用 .NET Core 自带的配置(Configuration)?
因为 ABP 设置管理可以做到三个层级,用户,租户和全局(系统级),同时 ABP 的设置管理只是做了一层封装,
具体的数据源可以是 .NET Core 自带的配置(Configuration),也可以是分布式配置。只不过需要我们自己去写扩展。另外建议大家对参数进行打包,比如邮件相关的参数可以封装在一个
EmailConfig类中,邮件 Host,用户名和密码都是该类的属性,而具体取值同时通过ISettingValueProvider来获取的。建议加入分布式缓存。
[Abp vNext 源码分析] - 11. 用户的自定义参数与配置的更多相关文章
- [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)
一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...
- [Abp vNext 源码分析] - 文章目录
一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...
- [Abp vNext 源码分析] - 3. 依赖注入与拦截器
一.简要说明 ABP vNext 框架在使用依赖注入服务的时候,是直接使用的微软提供的 Microsoft.Extensions.DependencyInjection 包.这里与原来的 ABP 框架 ...
- [Abp vNext 源码分析] - 1. 框架启动流程分析
一.简要说明 本篇文章主要剖析与讲解 Abp vNext 在 Web API 项目下的启动流程,让大家了解整个 Abp vNext 框架是如何运作的.总的来说 ,Abp vNext 比起 ABP 框架 ...
- [Abp vNext 源码分析] - 6. DDD 的应用层支持 (应用服务)
一.简要介绍 ABP vNext 针对于应用服务层,为我们单独设计了一个模块进行实现,即 Volo.Abp.Ddd.Application 模块. PS:最近博主也是在恶补 DDD 相关的知识,这里推 ...
- [Abp vNext 源码分析] - 7. 权限与验证
一.简要说明 在上篇文章里面,我们在 ApplicationService 当中看到了权限检测代码,通过注入 IAuthorizationService 就可以实现权限检测.不过跳转到源码才发现,这个 ...
- [Abp vNext 源码分析] - 12. 后台作业与后台工作者
一.简要说明 文章信息: 基于的 ABP vNext 版本:1.0.0 创作日期:2019 年 10 月 24 日晚 更新日期:暂无 ABP vNext 提供了后台工作者和后台作业的支持,基本实现与原 ...
- [Abp vNext 源码分析] - 14. EntityFramework Core 的集成
一.简要介绍 在以前的文章里面,我们介绍了 ABP vNext 在 DDD 模块定义了仓储的接口定义和基本实现.本章将会介绍,ABP vNext 是如何将 EntityFramework Core 框 ...
- [Abp vNext 源码分析] - 19. 多租户
一.简介 ABP vNext 原生支持多租户体系,可以让开发人员快速地基于框架开发 SaaS 系统.ABP vNext 实现多租户的思路也非常简单,通过一个 TenantId 来分割各个租户的数据,并 ...
随机推荐
- 即时聊天APP(四) - 联系人和会话
联系人和会话界面使用的是RecyclerView进行滑动显示,并将好友列表存储至数据库,以供下次登录时使用,RecyclerView在后面我会详细介绍,这里略过. 联系人初始化时读取数据库并展示: / ...
- VMware 虚拟机三种网络模式详解
一.前言 Vmware 为我们提供了三种网络工作模式,分别是:Bridged(桥接模式).NAT(网络地址转换模式).Host-only(仅主机模式). 二.VMware 的几个常见虚拟设备 打开 V ...
- [LeetCode] 由 “中缀表达式 --> 后缀表达式" 所想
如何利用栈解决问题. Ref: 如何在程序中将中缀表达式转换为后缀表达式? 本文的引申:如何手写语法分析器 实现调度场算法 “9+(3-1)*3+10/2” --> “9 3 1-3*+ 10 ...
- Spring Boot2 系列教程(八)Spring Boot 中配置 Https
https 现在已经越来越普及了,特别是做一些小程序或者公众号开发的时候,https 基本上都是刚需了. 不过一个 https 证书还是挺费钱的,个人开发者可以在各个云服务提供商那里申请一个免费的证书 ...
- 解决mysql不能在查询A表的同时,更新A表的问题
方法: 运用中间表 UPDATE 表名 SET 字段名 = '' WHERE id in (SELECT a.id FROM (SELECT id FROM 表名 WHERE ISNULL(字段名)) ...
- Webpack配置区分开发环境和生产环境
在项目开发的时候,我们通常会将程序分为开发环境和生产环境(或者叫线上环境),开发环境通常指的是我们正在开发的这个阶段所需要的一些环境配置,也就是方便我们开发人员调试开发的一种环境:生产环境通常指的是我 ...
- CentOS 8 正式发布
转载请注明:文章转载自 OSCHINA 社区 [http://www.oschina.net] 本文地址:https://www.oschina.net/news/110111/centos-8-re ...
- Redis数据库之数据基本管理操作
了解并掌握各种数据类型的命令操作方式,以及各种数据类型值的操作方式.同时,熟练记忆列表.哈希.集合和有序集合等数据类型的常用操作命令.能根据指令格式完成相应的指令操作. ①string数据类型的练习 ...
- Kurskal算法
Kruskal算法是以边为主要关注对象的最小生成树算法,是最小生成树最佳的算法实现. 其时间复杂度为O(ElogE)(E为边的数量),而Prime算法采用邻接矩阵的方法是O(V^2)(V为顶点数量). ...
- 浅谈JavaScript的闭包原理
在一般的教程里,都谈到子作用域可以访问到父级作用域,进而访问到父级作用域中的变量,具体是如何实现的,就不得不提及到函数堆栈和执行上下文. 举个例子,一个简单的闭包: 首先,我们可以知道,examp ...