ArcGIS JSAPI 源码解读 之 Accessor属性元数据

在上一步的解析过程中,Accessor对象首先为所有的属性生成了一个__accessorMetadata__容器,用来存储所有属性的元数据。之后,通过遍历构造Accessor对象传入的properties,从而为每一个响应式属性生成属性元数据,当然,从程序稳定性的角度,Accessor并不直接从用户传入的properties对象来构建,而是在内部再次通过类型标准化/类型推断,从而构建了标准的类型描述信息。

JavaScript 复制代码
// 遍历properties对象中的属性声明,并补充到__accessorMetadata__
for (const p in properties) {
	const typeDesc = predicateType(properties[p])
	property(typeDesc)(o.prototype, p);
}

predicateType函数从用户传入的对象信息中解析正确的数据元数据描述,一方面避免因用户传入错误的值而造成程序崩溃,另一方面也通过引入适配层,将元数据这一概念层从Accessor对象中解耦。

JavaScript 复制代码
function predicateType(property) {
	if (null == property) return { value: property };

	if (Array.isArray(property)) {
		return { type: [property[0]], value: null }
	}

	switch(typeof property) {
		case "object": {
			return property.constructor?.__accessorMetadata__ || property instanceof Date ? { type: property.constructor, value: property } : property;
		}
		case "boolean": {
			return { type: Boolean, value: property };
		}
		case "string": {
			return { type: String, value: property };
		}
		case "number": {
			return { type: Number, value: property };
		}
		case "function": {
			return { type: property, value: null };
		}
		default:
			return;
	}
}

property是一个装饰器函数,它接收上一步predicateType生成的格式化描述符作为初始化,返回一个真正的解析函数,这个解析函数有2个参数,分别是当前属性对象的元类型,以及用户在构造对象时真正传递的properties参数。

JavaScript 复制代码
function property(propertyType) {
	return function(objProto, propertyArgs) {
		if (objProto === Function.prototype) {
			throw new Error("Inappropriate use of @property() on a static field. Accessor does not support static properties.")
		}

		// Step 1: 生成当前属性的元数据并构建核心get/set/value能力
		const propDescriptor = Object.getOwnPropertyDescriptor(objProto, propertyArgs);

		/**
		 * 这里getPropertyMetadata将在当前对象下的__accessorMetadata__容器中生成一个属性的元数据
		 * 事实上这里不是一个很好的设计,将属性元数据的创建和获取职责合二为一了,不那么清晰
		 */
		const propMeta = getPropertyMetadata(objProto, propertyArgs);
		if (propDescriptor) {
			if (propDescriptor.get || propDescriptor.set) {
				propMeta.get = propDescriptor.get || propMeta.get;
				propMeta.set = propDescriptor.set || propMeta.set;
			} else {
				if ("value" in propMeta) {
					if ("value" in propertyType) {
						propMeta.value = propertyType.value = propDescriptor.value;
					}
				}
			}
		}

		// Step 2: 继续构造property所提供的其他附加定义,我们后面详细介绍
	}
}

上面提到,getPropertyMetadata函数将属性元数据的创建和获取职责合二为一,这让函数的职能变得不是那么清晰,当然这也有函数本身确实逻辑相当简单的原因。

JavaScript 复制代码
function getPropertyMetadata(objProto, propertyArgs) {
	const properties = getPropertiesMetadata(objProto)
	let propertyMeta = properties[propertyArgs]
	if (!propertyMeta) {

		// 这里可以看到property的元数据就是一个普通POJO,确实相当简单但够用
		propertyMeta = properties[propertyArgs] = {}
	}

	return propertyMeta
}

Accessor 中的附加行为定义


1. 只读属性(Read-Only)

通过在对象定义时为属性声明readOnly属性,可以指定特定的属性为只读,从而禁止在后续调用set函数改变初始值。属性元数据中同样复制了这个标志位。

JavaScript 复制代码
if (propertyType.readOnly != null) {
	propMeta.readOnly = propertyType.readOnly
}

2. 代理属性(aliasOf)

Accessor提供了一种属性/函数代理机制,能通过aliasOf声明字符串路径简化访问对象内部多层嵌套的子对象属性/函数。在属性元数据中,针对aliasOf别名同样进行了解析和存储。

JavaScript 复制代码
const alias = propertyType.aliasOf;
if (alias) {
	const aliasSource = "string" == typeof alias ? alias : alias.source;
	const aliasOverride = "string" == typeof alias ? null : !0 === alias.overridable;

	propMeta.dependsOn = [aliasSource]
	let targetPropertyName;

	propMeta.get = function() {
		let propertyValue = get(this, aliasSource)

		// 2. 为了兼容针对子对象函数的代理,这里做了额外的处理
		// 这里同样并不是一个很好的实现,我认为应该可以统一放到get函数中去处理这个逻辑
		if ("function" == typeof propertyValue) {
			if (!targetPropertyName) {
				targetPropertyName = aliasSource.split('.').slice(0, -1).join('.')
				const funcValue = get(this, targetPropertyName)
				if (funcValue) {
					propertyValue = propertyValue.bind(funcValue)
				}
			}
		}

		// 1. 如果只是针对对象属性的别名代理,那么就直接返回真实的属性值
		return propertyValue
	}

	if (!propMeta.readOnly) {
		propMeta.set = aliasOverride 
			? function() {
				this._override(propertyArgs, aliasSource)
			  } 
			: function(val) {
				set(this, aliasSource, val)
			  }
	}
	
}

3. 自动转型(type & cast)

前面我们了解到AutoCast作为整个Maps SDK的核心能力之一,为SDK内部类型到通用的JSON或者其他表达式类型的转换提供了底层机制保证,然而,我们并不能保证所有从SDK对象到JSON表达式的转换都是简单直观的(尤为典型的就是SDK中大量的Well-Known ID机制,通过JSON易于表达的、简单的枚举值来声明使用内置的对象类型),很多时候往往需要一种定制化的converter机制,好在Maps SDK已经考虑到了这一点,提供显式的转换行为声明,这也是通过属性元数据来存储的。

JavaScript 复制代码
const targetType = propertyType.type
const targetTypes = propertyType.types

if (!propMeta.cast) {
	/**
	 * 在SDK文档中明确提到
	 * The `type` metadata automatically creates an appropriate [`cast`] for Accessor and primitive types if it is not already set."
	 */
	 if (targetType) {
		 propMeta.cast = autocast(targetType)
	 } else if (targetTypes) {
		 if (Array.isArray(targetTypes)) {
			 propMeta.cast = ensureArrayTyped(ensureOneOfType(targetTypes[0]))
		 } else {
			 propMeta.cast = ensureOneOfType(targetTypes)
		 }
	 }

	mergeProperty(propMeta, propertyType)
	if (propertyType.range) {
		propMeta.cast = ensureRange(propMeta.cast, propertyType.range)
	}
} 

至此,我们已经了解了Accessor类型中关于元数据的一切,如果你感兴趣,不妨动手去尝试实现一个自己的"企业级"对象核心库。

如果你觉得本文对你有些许启发,请持续关注我的公众号"戈伊星球"吧!

相关推荐
汪子熙25 分钟前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
Envyᥫᩣ34 分钟前
《ASP.NET Web Forms 实现视频点赞功能的完整示例》
前端·asp.net·音视频·视频点赞
Мартин.5 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。6 小时前
案例-表白墙简单实现
前端·javascript·css
数云界6 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd6 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常6 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer6 小时前
Vite:为什么选 Vite
前端
小御姐@stella6 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing6 小时前
【React】增量传输与渲染
前端·javascript·面试