优化组件创建逻辑;改用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 * @date 2023/03/06 12:08:35
*/ */
export const FINALIZED = Symbol('finalized') const boolMap = {}
export const UPDATE = Symbol('update')
;[
'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 WC_PART = Symbol('wc_path')
export const NO_CHANGE = Symbol('wc-noChange') export const NO_CHANGE = Symbol('wc-noChange')
export const NOTHING = Symbol('wc-nothing') 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 = ` export const RESET_CSS_STYLE = `
* {box-sizing: border-box;margin: 0;padding: 0;} * {box-sizing: border-box;margin: 0;padding: 0;}
@ -19,6 +53,7 @@ export const DEFAULT_CONVERTER = {
toAttribute(value, type) { toAttribute(value, type) {
switch (type) { switch (type) {
case Boolean: case Boolean:
// console.log(this, '>>>', value)
value = value ? '' : null value = value ? '' : null
break break
case Object: case Object:
@ -57,7 +92,9 @@ export function notEqual(value, old) {
export const DEFAULT_PROPERTY_DECLARATION = { export const DEFAULT_PROPERTY_DECLARATION = {
attribute: true, attribute: true,
type: String, type: String,
converter: DEFAULT_CONVERTER, formater: DEFAULT_CONVERTER,
// converter: DEFAULT_CONVERTER,
reflect: false, reflect: false,
hasChanged: notEqual hasChanged: notEqual,
default: ''
} }

View File

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