优化组件创建逻辑;改用Symbol解决原型方法被覆盖的风险

pull/1/head
yutent 2023-03-10 18:38:52 +08:00
parent 08d8eb3b74
commit a1f4823537
2 changed files with 113 additions and 100 deletions

View File

@ -4,11 +4,45 @@
* @date 2023/03/06 12:08:35
*/
export const FINALIZED = Symbol('finalized')
export const UPDATE = Symbol('update')
const boolMap = {}
;[
'autofocus',
'autoplay',
'async',
'allowTransparency',
'checked',
'controls',
'declare',
'disabled',
'defer',
'defaultChecked',
'defaultSelected',
'contentEditable',
'isMap',
'loop',
'multiple',
'noHref',
'noResize',
'noShade',
'open',
'readOnly',
'selected'
].forEach(function (name) {
boolMap[name.toLowerCase()] = name
})
export { boolMap }
export const WC_PART = Symbol('wc_path')
export const NO_CHANGE = Symbol('wc-noChange')
export const NOTHING = Symbol('wc-nothing')
export const __finalized__ = Symbol('finalized')
export const __update__ = Symbol('update')
export const __init__ = Symbol('init')
export const __props__ = Symbol('props')
export const __changed_props__ = Symbol('changed_props')
export const __mounted__ = Symbol('mounted')
export const RESET_CSS_STYLE = `
* {box-sizing: border-box;margin: 0;padding: 0;}
@ -19,6 +53,7 @@ export const DEFAULT_CONVERTER = {
toAttribute(value, type) {
switch (type) {
case Boolean:
// console.log(this, '>>>', value)
value = value ? '' : null
break
case Object:
@ -57,7 +92,9 @@ export function notEqual(value, old) {
export const DEFAULT_PROPERTY_DECLARATION = {
attribute: true,
type: String,
converter: DEFAULT_CONVERTER,
formater: DEFAULT_CONVERTER,
// converter: DEFAULT_CONVERTER,
reflect: false,
hasChanged: notEqual
hasChanged: notEqual,
default: ''
}

View File

@ -4,11 +4,16 @@
* @date 2023/03/07 18:10:43
*/
import {
FINALIZED,
UPDATE,
DEFAULT_CONVERTER,
DEFAULT_PROPERTY_DECLARATION,
notEqual
notEqual,
boolMap,
__finalized__,
__update__,
__init__,
__props__,
__changed_props__,
__mounted__
} from './constants.js'
import { css, adoptStyles } from './css.js'
import { render, html, svg } from './html.js'
@ -24,78 +29,71 @@ export {
export { html, css, svg, bind, unbind }
export class Component extends HTMLElement {
constructor() {
super()
this.__instanceProperties = new Map()
this.isUpdatePending = false
this.hasUpdated = false
this.__reflectingProperty = null
this._initialize()
this.created && this.created()
}
static addInitializer(initializer) {
this.finalize()
if (!this._initializers) {
this._initializers = []
}
this._initializers.push(initializer)
}
/**
* 声明可监听变化的属性列表
* @return list<Array>
*/
static get observedAttributes() {
let list = []
this.finalize()
const attributes = []
this.elementProperties.forEach((v, p) => {
const attr = this.__attributeNameForProperty(p, v)
if (attr !== void 0) {
this.__attributeToPropertyMap.set(attr, p)
attributes.push(attr)
this[__props__].forEach((options, prop) => {
if (options) {
options.watch && list.push(k.toLowerCase())
} else {
list.push(k.toLowerCase())
}
})
return attributes
return list
}
static createProperty(name, options = DEFAULT_PROPERTY_DECLARATION) {
if (options.state) {
options.attribute = false
}
this.elementProperties.set(name, options)
this[__props__].set(name, options)
let key = Symbol(name)
let descriptor = this.getPropertyDescriptor(name, key, options)
this.prototype[key] = options.default
Object.defineProperty(this.prototype, name, descriptor)
}
static getPropertyDescriptor(name, key, options) {
return {
let descriptor = {
get() {
return this[key]
},
set(value) {
const oldValue = this[name]
let oldValue = this[key]
this[key] = value
this.requestUpdate(name, oldValue, options)
},
configurable: true
enumerable: false
}
Object.defineProperty(this.prototype, name, descriptor)
// this.prototype[name] = options.default
}
static getPropertyOptions(name) {
return this.elementProperties.get(name) || DEFAULT_PROPERTY_DECLARATION
}
// 处理静态声明
static finalize() {
if (this[FINALIZED]) {
if (this[__finalized__]) {
return false
}
this[FINALIZED] = true
this[__finalized__] = true
this[__props__] = new Map()
this.elementProperties = new Map()
this.__attributeToPropertyMap = new Map()
if (this.hasOwnProperty('props')) {
for (let k in this.props) {
if (boolMap[k] && k !== boolMap[k]) {
this.props[boolMap[k]] = this.props[k]
delete this.props[k]
k = boolMap[k]
}
this.createProperty(k, this.props[k])
}
}
delete this.props
return true
}
@ -109,33 +107,25 @@ export class Component extends HTMLElement {
? name.toLowerCase()
: void 0
}
_initialize() {
this.__updatePromise = new Promise(res => (this.enableUpdating = res))
this._$changedProperties = new Map()
this.__saveInstanceProperties()
this.requestUpdate()
this.constructor._initializers?.forEach(i => i(this))
}
addController(controller) {
if (!this.__controllers) {
this.__controllers = []
}
this.__controllers.push(controller)
if (this.root !== void 0 && this.isConnected) {
controller.hostConnected?.call(controller)
}
constructor() {
super()
this.isUpdatePending = false
this.__reflectingProperty = null
this[__mounted__] = false
this[__init__]()
this.created && this.created()
}
removeController(controller) {
this.__controllers?.splice(this.__controllers.indexOf(controller) >>> 0, 1)
}
__saveInstanceProperties() {
this.constructor.elementProperties.forEach((_v, p) => {
if (this.hasOwnProperty(p)) {
this.__instanceProperties.set(p, this[p])
delete this[p]
}
[__init__]() {
this.__updatePromise = new Promise(res => (this.enableUpdating = res))
this[__changed_props__] = new Map() // 记录本次变化的属性
// 初始化 props
this.constructor[__props__].forEach((options, prop) => {
this[prop] = options.default
})
this.requestUpdate()
}
connectedCallback() {
@ -145,14 +135,10 @@ export class Component extends HTMLElement {
this.enableUpdating(true)
this.__controllers?.forEach(it => it.hostConnected?.call(it))
this.__childPart?.setConnected(true)
this.mounted && this.mounted()
}
enableUpdating(_requestedUpdate) {}
disconnectedCallback() {
this.__controllers?.forEach(it => it.hostDisconnected?.call(it))
this.__childPart?.setConnected(false)
}
attributeChangedCallback(name, _old, value) {
@ -194,11 +180,11 @@ export class Component extends HTMLElement {
requestUpdate(name, oldValue, options) {
let shouldRequestUpdate = true
if (name !== void 0) {
options = options || this.constructor.getPropertyOptions(name)
options = options || this.constructor[__props__][name]
const hasChanged = options.hasChanged || notEqual
if (hasChanged(this[name], oldValue)) {
if (!this._$changedProperties.has(name)) {
this._$changedProperties.set(name, oldValue)
if (!this[__changed_props__].has(name)) {
this[__changed_props__].set(name, oldValue)
}
if (options.reflect === true && this.__reflectingProperty !== name) {
if (this.__reflectingProperties === void 0) {
@ -235,15 +221,9 @@ export class Component extends HTMLElement {
return
}
if (this.__instanceProperties) {
this.__instanceProperties.forEach((v, p) => (this[p] = v))
this.__instanceProperties = void 0
}
const changedProperties = this._$changedProperties
const changedProperties = this[__changed_props__]
try {
this.__controllers?.forEach(it => it.hostUpdate?.call(it))
this[UPDATE](changedProperties)
this[__update__](changedProperties)
this._$didUpdate(changedProperties)
} catch (e) {
this.__markUpdated()
@ -251,16 +231,14 @@ export class Component extends HTMLElement {
}
}
_$didUpdate(changedProperties) {
this.__controllers?.forEach(it => it.hostUpdated?.call(it))
if (!this.hasUpdated) {
this.hasUpdated = true
this.firstUpdated(changedProperties)
if (!this[__mounted__]) {
this[__mounted__] = true
this.mounted && this.mounted()
}
this.updated(changedProperties)
}
__markUpdated() {
this._$changedProperties = new Map()
this[__changed_props__] = new Map()
this.isUpdatePending = false
}
get updateComplete() {
@ -270,8 +248,8 @@ export class Component extends HTMLElement {
return this.__updatePromise
}
[UPDATE](_changedProperties) {
let value = this.render()
[__update__](_changedProperties) {
let htmlText = this.render()
if (this.__reflectingProperties !== void 0) {
this.__reflectingProperties.forEach((v, k) =>
@ -280,14 +258,12 @@ export class Component extends HTMLElement {
this.__reflectingProperties = void 0
}
this.__markUpdated()
this.__childPart = render(value, this.root, {
this.__childPart = render(htmlText, this.root, {
host: this,
isConnected: !this.hasUpdated && this.isConnected
isConnected: !this[__mounted__] && this.isConnected
})
}
updated(_changedProperties) {}
firstUpdated(_changedProperties) {}
$on(type, callback) {
return bind(this, type, callback)