在上一步的解析过程中,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
类型中关于元数据的一切,如果你感兴趣,不妨动手去尝试实现一个自己的"企业级"对象核心库。
如果你觉得本文对你有些许启发,请持续关注我的公众号"戈伊星球"吧!